From e115a3ab15091de420b5acfb13db7f87cea4a5de Mon Sep 17 00:00:00 2001 From: tema Date: Sat, 6 Sep 2025 10:01:52 +0900 Subject: [PATCH] Add daily SIM usage tracking and persistence functionality - Introduced SimUsageDaily model to store daily usage snapshots for SIM accounts. - Updated SimManagementService to persist daily usage data and clean up previous months' records. - Enhanced error handling for usage persistence failures. - Updated SubscriptionsModule to include SimUsageStoreService for managing usage data. - Improved user interface text for clarity in SIM management pages. --- apps/bff/prisma/schema.prisma | 14 ++++++ .../subscriptions/sim-management.service.ts | 18 +++++++ .../subscriptions/sim-usage-store.service.ts | 49 +++++++++++++++++++ .../src/subscriptions/subscriptions.module.ts | 3 +- .../subscriptions/[id]/sim/cancel/page.tsx | 48 ++++++++++++++++++ .../[id]/sim/change-plan/page.tsx | 3 +- .../subscriptions/[id]/sim/top-up/page.tsx | 3 +- .../sim-management/components/SimActions.tsx | 8 ++- 8 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 apps/bff/src/subscriptions/sim-usage-store.service.ts create mode 100644 apps/portal/src/app/subscriptions/[id]/sim/cancel/page.tsx diff --git a/apps/bff/prisma/schema.prisma b/apps/bff/prisma/schema.prisma index da8f2a34..7bd8f942 100644 --- a/apps/bff/prisma/schema.prisma +++ b/apps/bff/prisma/schema.prisma @@ -170,3 +170,17 @@ enum AuditAction { MFA_DISABLED API_ACCESS } + +// Per-SIM daily usage snapshot used to build full-month charts +model SimUsageDaily { + id Int @id @default(autoincrement()) + account String + date DateTime @db.Date + usageMb Float + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([account, date]) + @@index([account, date]) + @@map("sim_usage_daily") +} diff --git a/apps/bff/src/subscriptions/sim-management.service.ts b/apps/bff/src/subscriptions/sim-management.service.ts index 1a0d06af..20f8d057 100644 --- a/apps/bff/src/subscriptions/sim-management.service.ts +++ b/apps/bff/src/subscriptions/sim-management.service.ts @@ -4,6 +4,7 @@ import { FreebititService } from '../vendors/freebit/freebit.service'; import { MappingsService } from '../mappings/mappings.service'; import { SubscriptionsService } from './subscriptions.service'; import { SimDetails, SimUsage, SimTopUpHistory } from '../vendors/freebit/interfaces/freebit.types'; +import { SimUsageStoreService } from './sim-usage-store.service'; import { getErrorMessage } from '../common/utils/error.util'; export interface SimTopUpRequest { @@ -42,6 +43,7 @@ export class SimManagementService { private readonly mappingsService: MappingsService, private readonly subscriptionsService: SubscriptionsService, @Inject(Logger) private readonly logger: Logger, + private readonly usageStore: SimUsageStoreService, ) {} /** @@ -184,6 +186,22 @@ export class SimManagementService { const { account } = await this.validateSimSubscription(userId, subscriptionId); const simUsage = await this.freebititService.getSimUsage(account); + + // Persist today's usage for monthly charts and cleanup previous months + try { + await this.usageStore.upsertToday(account, simUsage.todayUsageMb); + await this.usageStore.cleanupPreviousMonths(); + const stored = await this.usageStore.getLastNDays(account, 30); + if (stored.length > 0) { + simUsage.recentDaysUsage = stored.map(d => ({ + date: d.date, + usageKb: Math.round(d.usageMb * 1024), + usageMb: d.usageMb, + })); + } + } catch (e) { + this.logger.warn('SIM usage persistence failed (non-fatal)', { account, error: getErrorMessage(e) }); + } this.logger.log(`Retrieved SIM usage for subscription ${subscriptionId}`, { userId, diff --git a/apps/bff/src/subscriptions/sim-usage-store.service.ts b/apps/bff/src/subscriptions/sim-usage-store.service.ts new file mode 100644 index 00000000..af72a980 --- /dev/null +++ b/apps/bff/src/subscriptions/sim-usage-store.service.ts @@ -0,0 +1,49 @@ +import { Injectable, Inject } from "@nestjs/common"; +import { PrismaService } from "../common/prisma/prisma.service"; +import { Logger } from "nestjs-pino"; + +@Injectable() +export class SimUsageStoreService { + constructor( + private readonly prisma: PrismaService, + @Inject(Logger) private readonly logger: Logger, + ) {} + + private normalizeDate(date?: Date): Date { + const d = date ? new Date(date) : new Date(); + // strip time to YYYY-MM-DD + const iso = d.toISOString().split('T')[0]; + return new Date(iso + 'T00:00:00.000Z'); + } + + async upsertToday(account: string, usageMb: number, date?: Date): Promise { + const day = this.normalizeDate(date); + try { + await (this.prisma as any).simUsageDaily.upsert({ + where: { account_date: { account, date: day } as any }, + update: { usageMb }, + create: { account, date: day, usageMb }, + }); + } catch (e: any) { + this.logger.error("Failed to upsert daily usage", { account, error: e?.message }); + } + } + + async getLastNDays(account: string, days = 30): Promise> { + const end = this.normalizeDate(); + const start = new Date(end); + start.setUTCDate(end.getUTCDate() - (days - 1)); + const rows = await (this.prisma as any).simUsageDaily.findMany({ + where: { account, date: { gte: start, lte: end } }, + orderBy: { date: 'desc' }, + }) as Array<{ date: Date; usageMb: number }>; + return rows.map((r) => ({ date: r.date.toISOString().split('T')[0], usageMb: r.usageMb })); + } + + async cleanupPreviousMonths(): Promise { + const now = new Date(); + const firstOfMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)); + const result = await (this.prisma as any).simUsageDaily.deleteMany({ where: { date: { lt: firstOfMonth } } }); + return result.count; + } +} diff --git a/apps/bff/src/subscriptions/subscriptions.module.ts b/apps/bff/src/subscriptions/subscriptions.module.ts index aeed57a7..40e3c143 100644 --- a/apps/bff/src/subscriptions/subscriptions.module.ts +++ b/apps/bff/src/subscriptions/subscriptions.module.ts @@ -2,6 +2,7 @@ import { Module } from "@nestjs/common"; import { SubscriptionsController } from "./subscriptions.controller"; import { SubscriptionsService } from "./subscriptions.service"; import { SimManagementService } from "./sim-management.service"; +import { SimUsageStoreService } from "./sim-usage-store.service"; import { WhmcsModule } from "../vendors/whmcs/whmcs.module"; import { MappingsModule } from "../mappings/mappings.module"; import { FreebititModule } from "../vendors/freebit/freebit.module"; @@ -9,6 +10,6 @@ import { FreebititModule } from "../vendors/freebit/freebit.module"; @Module({ imports: [WhmcsModule, MappingsModule, FreebititModule], controllers: [SubscriptionsController], - providers: [SubscriptionsService, SimManagementService], + providers: [SubscriptionsService, SimManagementService, SimUsageStoreService], }) export class SubscriptionsModule {} diff --git a/apps/portal/src/app/subscriptions/[id]/sim/cancel/page.tsx b/apps/portal/src/app/subscriptions/[id]/sim/cancel/page.tsx new file mode 100644 index 00000000..abc455fd --- /dev/null +++ b/apps/portal/src/app/subscriptions/[id]/sim/cancel/page.tsx @@ -0,0 +1,48 @@ +\"use client\"; + +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { useState } from "react"; +import { DashboardLayout } from "@/components/layout/dashboard-layout"; +import { authenticatedApi } from "@/lib/api"; + +export default function SimCancelPage() { + const params = useParams(); + const subscriptionId = parseInt(params.id as string); + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState(null); + const [error, setError] = useState(null); + + const submit = async () => { + setLoading(true); + setMessage(null); + setError(null); + try { + await authenticatedApi.post(`/subscriptions/${subscriptionId}/sim/cancel`, {}); + setMessage("SIM service cancelled successfully"); + } catch (e: any) { + setError(e instanceof Error ? e.message : "Failed to cancel SIM service"); + } finally { + setLoading(false); + } + }; + + return ( + +
+
+ ← Back to SIM Management +
+
+

Cancel SIM

+

Cancel SIM: Permanently cancel your SIM service. This action cannot be undone and will terminate your service immediately.

+ + {message &&
{message}
} + {error &&
{error}
} + +
+ This is a destructive action. Your service will be terminated immediately. +
+ +
+

Change Plan

-

Switch to a different data plan. Important: request before the 25th; takes effect on the 1st.

- +

Change Plan: Switch to a different data plan. Important: Plan changes must be requested before the 25th of the month. Changes will take effect on the 1st of the following month.

{message &&
{message}
} {error &&
{error}
} diff --git a/apps/portal/src/app/subscriptions/[id]/sim/top-up/page.tsx b/apps/portal/src/app/subscriptions/[id]/sim/top-up/page.tsx index 351665e0..7d83e93f 100644 --- a/apps/portal/src/app/subscriptions/[id]/sim/top-up/page.tsx +++ b/apps/portal/src/app/subscriptions/[id]/sim/top-up/page.tsx @@ -47,8 +47,7 @@ export default function SimTopUpPage() {

Top Up Data

-

Add data quota to your SIM service

- +

Top Up Data: Add additional data quota to your SIM service. You can choose the amount and schedule it for later if needed.

{message &&
{message}
} {error &&
{error}
} diff --git a/apps/portal/src/features/sim-management/components/SimActions.tsx b/apps/portal/src/features/sim-management/components/SimActions.tsx index 0f3c2333..433384d0 100644 --- a/apps/portal/src/features/sim-management/components/SimActions.tsx +++ b/apps/portal/src/features/sim-management/components/SimActions.tsx @@ -206,8 +206,12 @@ export function SimActions({