/** * 📦 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 | 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([]) const total = ref(0) const isLoading = ref(false) const error = ref(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 = { 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 { 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 { 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 { 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, } })