Files
service-finder/frontend_app/src/stores/adminPackages.ts
2026-06-24 11:29:45 +00:00

260 lines
7.1 KiB
TypeScript

/**
* 📦 Admin Packages Store
*
* Pinia store for managing SubscriptionTier packages.
* Mirrors the backend admin_packages.py API endpoints.
*
* Endpoints:
* GET /admin/packages/ — List all packages
* POST /admin/packages/ — Create a new package
* PATCH /admin/packages/{id} — Update a package
* DELETE /admin/packages/{id} — Soft-delete (deactivate) a package
*/
import { defineStore } from 'pinia'
import { ref } from 'vue'
import api from '../api/axios'
// ── TypeScript Interfaces (mirroring backend Pydantic schemas) ───────────────
export interface PricingModel {
monthly_price: number
yearly_price: number
currency: string
credit_price?: number | null
}
export interface PricingZoneModel {
monthly_price: number
yearly_price: number
currency: string
credit_price?: number | null
}
export interface MarketingModel {
subtitle?: string | null
badge?: string | null
highlight_color?: string | null
}
export interface AllowancesModel {
max_vehicles: number
max_garages: number
monthly_free_credits: number
}
export interface LifecycleModel {
is_public: boolean
available_until?: string | null
}
export interface AffiliateModel {
commission_rate_percent: number
referral_bonus_credits: number
}
export interface SubscriptionRulesModel {
type: 'private' | 'corporate'
display_name?: string | null
pricing: PricingModel
pricing_zones?: Record<string, PricingZoneModel> | null
allowances: AllowancesModel
entitlements: string[]
affiliate: AffiliateModel
lifecycle?: LifecycleModel | null
marketing?: MarketingModel | null
}
export interface SubscriptionTierItem {
id: number
name: string
rules: SubscriptionRulesModel
is_custom: boolean
}
export interface SubscriptionTierListResponse {
total: number
tiers: SubscriptionTierItem[]
}
export interface SubscriptionTierCreatePayload {
name: string
rules: SubscriptionRulesModel
is_custom?: boolean
}
export interface SubscriptionTierUpdatePayload {
name?: string
rules?: SubscriptionRulesModel
is_custom?: boolean
}
export interface DeletePackageResponse {
status: string
message: string
tier_id: number
is_public: boolean
}
// ── Available Entitlements (service codes) ───────────────────────────────────
export const AVAILABLE_ENTITLEMENTS: string[] = [
'SRV_DATA_EXPORT',
'SRV_AI_UPLOAD',
'SRV_ACCOUNTING_SYNC',
'SRV_API_ACCESS',
'SRV_MULTI_USER',
'SRV_PRIORITY_SUPPORT',
'SRV_ADVANCED_ANALYTICS',
'SRV_CUSTOM_REPORTS',
'SRV_GARAGE_SHARING',
'SRV_SERVICE_HISTORY',
'SRV_FUEL_MANAGEMENT',
'SRV_MAINTENANCE_ALERTS',
'SRV_ODOMETER_TRACKING',
'SRV_DOCUMENT_STORAGE',
'SRV_INSURANCE_INTEGRATION',
]
// ── Store ────────────────────────────────────────────────────────────────────
export const useAdminPackagesStore = defineStore('adminPackages', () => {
// ── State ────────────────────────────────────────────────────────────────
const tiers = ref<SubscriptionTierItem[]>([])
const total = ref(0)
const isLoading = ref(false)
const error = ref<string | null>(null)
// ── Actions ──────────────────────────────────────────────────────────────
/**
* Fetch all subscription packages from GET /admin/packages/.
*/
async function fetchPackages(params: {
skip?: number
limit?: number
include_hidden?: boolean
type_filter?: string
date_from?: string
date_until?: string
} = {}) {
isLoading.value = true
error.value = null
try {
const queryParams: Record<string, any> = {
skip: params.skip ?? 0,
limit: params.limit ?? 100,
}
if (params.include_hidden) {
queryParams.include_hidden = true
}
if (params.type_filter) {
queryParams.type_filter = params.type_filter
}
if (params.date_from) {
queryParams.date_from = params.date_from
}
if (params.date_until) {
queryParams.date_until = params.date_until
}
const res = await api.get('/admin/packages', { params: queryParams })
const data = res.data as SubscriptionTierListResponse
console.log('[AdminPackages] API response:', data)
console.log('[AdminPackages] tiers array:', data.tiers)
console.log('[AdminPackages] tiers length:', data.tiers?.length)
tiers.value = data.tiers
total.value = data.total
return data
} catch (err: any) {
error.value = err.response?.data?.detail || 'Failed to fetch packages'
throw err
} finally {
isLoading.value = false
}
}
/**
* Create a new subscription package via POST /admin/packages/.
* NOTE: Do NOT send the `id` field — PostgreSQL auto-increments it.
*/
async function createPackage(payload: SubscriptionTierCreatePayload): Promise<SubscriptionTierItem> {
isLoading.value = true
error.value = null
try {
const res = await api.post('/admin/packages', payload)
const created = res.data as SubscriptionTierItem
// Refresh the list
await fetchPackages({ include_hidden: true })
return created
} catch (err: any) {
error.value = err.response?.data?.detail || 'Failed to create package'
throw err
} finally {
isLoading.value = false
}
}
/**
* Update an existing package via PATCH /admin/packages/{id}.
*/
async function updatePackage(
tierId: number,
payload: SubscriptionTierUpdatePayload,
): Promise<SubscriptionTierItem> {
isLoading.value = true
error.value = null
try {
const res = await api.patch(`/admin/packages/${tierId}`, payload)
const updated = res.data as SubscriptionTierItem
// Refresh the list
await fetchPackages({ include_hidden: true })
return updated
} catch (err: any) {
error.value = err.response?.data?.detail || 'Failed to update package'
throw err
} finally {
isLoading.value = false
}
}
/**
* Soft-delete (deactivate) a package via DELETE /admin/packages/{id}.
* Sets lifecycle.is_public = false.
*/
async function deletePackage(tierId: number): Promise<DeletePackageResponse> {
isLoading.value = true
error.value = null
try {
const res = await api.delete(`/admin/packages/${tierId}`)
const result = res.data as DeletePackageResponse
// Refresh the list
await fetchPackages({ include_hidden: true })
return result
} catch (err: any) {
error.value = err.response?.data?.detail || 'Failed to delete package'
throw err
} finally {
isLoading.value = false
}
}
return {
// State
tiers,
total,
isLoading,
error,
// Actions
fetchPackages,
createPackage,
updatePackage,
deletePackage,
}
})