From a9bff8c82381b6f8a568656d54d8676a1a2aee84 Mon Sep 17 00:00:00 2001 From: barsa Date: Mon, 29 Sep 2025 15:26:54 +0900 Subject: [PATCH] Refactor OpenAPI generation and invoice handling in BFF. Update package.json scripts for type generation and streamline post-install processes. Enhance OpenAPI schema with new invoice endpoints, including retrieval, payment methods, and gateways. Improve invoice transformer logic to calculate overdue days and normalize statuses. Update frontend components for better user experience and consistency in displaying invoice details and payment methods. --- apps/bff/openapi/openapi.json | 569 ++- apps/bff/scripts/openapi.module.ts | 28 +- apps/bff/src/core/validation/index.ts | 35 +- .../services/invoice-transformer.service.ts | 24 +- .../services/payment-transformer.service.ts | 9 +- .../transformers/utils/status-normalizer.ts | 62 + .../whmcs/types/whmcs-api.types.ts | 4 +- .../modules/invoices/invoices.controller.ts | 19 +- .../InvoiceDetail/InvoiceSummaryBar.tsx | 32 +- .../components/InvoiceList/InvoiceList.tsx | 111 +- .../components/InvoiceTable/InvoiceTable.tsx | 304 +- .../billing/components/PaymentMethodCard.tsx | 126 +- .../src/features/billing/hooks/useBilling.ts | 14 + .../features/billing/views/PaymentMethods.tsx | 216 +- .../features/catalog/views/InternetPlans.tsx | 10 +- .../portal/src/lib/api/__generated__/types.ts | 3153 +++-------------- package.json | 5 +- .../domain/src/validation/shared/entities.ts | 1 + scripts/generate-frontend-types.sh | 16 + 19 files changed, 1754 insertions(+), 2984 deletions(-) create mode 100755 scripts/generate-frontend-types.sh diff --git a/apps/bff/openapi/openapi.json b/apps/bff/openapi/openapi.json index 3f75246f..3d939243 100644 --- a/apps/bff/openapi/openapi.json +++ b/apps/bff/openapi/openapi.json @@ -15,6 +15,300 @@ "System" ] } + }, + "/invoices": { + "get": { + "description": "Retrieves invoices for the authenticated user with pagination and optional status filtering", + "operationId": "InvoicesController_getInvoices", + "parameters": [ + { + "name": "status", + "required": false, + "in": "query", + "description": "Filter by invoice status", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Items per page (default: 10)", + "schema": { + "type": "number" + } + }, + { + "name": "page", + "required": false, + "in": "query", + "description": "Page number (default: 1)", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "List of invoices with pagination", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceListDto" + } + } + } + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "Get paginated list of user invoices", + "tags": [ + "invoices" + ] + } + }, + "/invoices/payment-methods": { + "get": { + "description": "Retrieves all saved payment methods for the authenticated user", + "operationId": "InvoicesController_getPaymentMethods", + "parameters": [], + "responses": { + "200": { + "description": "List of payment methods" + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "Get user payment methods", + "tags": [ + "invoices" + ] + } + }, + "/invoices/payment-gateways": { + "get": { + "description": "Retrieves all active payment gateways available for payments", + "operationId": "InvoicesController_getPaymentGateways", + "parameters": [], + "responses": { + "200": { + "description": "List of payment gateways" + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "Get available payment gateways", + "tags": [ + "invoices" + ] + } + }, + "/invoices/payment-methods/refresh": { + "post": { + "description": "Invalidates and refreshes payment methods cache for the current user", + "operationId": "InvoicesController_refreshPaymentMethods", + "parameters": [], + "responses": { + "200": { + "description": "Payment methods cache refreshed" + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "Refresh payment methods cache", + "tags": [ + "invoices" + ] + } + }, + "/invoices/{id}": { + "get": { + "description": "Retrieves detailed information for a specific invoice", + "operationId": "InvoicesController_getInvoiceById", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Invoice ID", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Invoice details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceDto" + } + } + } + }, + "404": { + "description": "Invoice not found" + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "Get invoice details by ID", + "tags": [ + "invoices" + ] + } + }, + "/invoices/{id}/subscriptions": { + "get": { + "description": "Retrieves all subscriptions that are referenced in the invoice items", + "operationId": "InvoicesController_getInvoiceSubscriptions", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Invoice ID", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "List of related subscriptions" + }, + "404": { + "description": "Invoice not found" + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "Get subscriptions related to an invoice", + "tags": [ + "invoices" + ] + } + }, + "/invoices/{id}/sso-link": { + "post": { + "description": "Generates a single sign-on link to view/pay the invoice or download PDF in WHMCS", + "operationId": "InvoicesController_createSsoLink", + "parameters": [ + { + "name": "target", + "required": false, + "in": "query", + "description": "Link target: view invoice, download PDF, or go to payment page (default: view)", + "schema": { + "enum": [ + "view", + "download", + "pay" + ], + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "description": "Invoice ID", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "SSO link created successfully" + }, + "404": { + "description": "Invoice not found" + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "Create SSO link for invoice", + "tags": [ + "invoices" + ] + } + }, + "/invoices/{id}/payment-link": { + "post": { + "description": "Generates a payment link for the invoice with a specific payment method or gateway", + "operationId": "InvoicesController_createPaymentLink", + "parameters": [ + { + "name": "gatewayName", + "required": false, + "in": "query", + "description": "Payment gateway name", + "schema": { + "type": "string" + } + }, + { + "name": "paymentMethodId", + "required": false, + "in": "query", + "description": "Payment method ID", + "schema": { + "type": "number" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "description": "Invoice ID", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Payment link created successfully" + }, + "404": { + "description": "Invoice not found" + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "Create payment link for invoice with payment method", + "tags": [ + "invoices" + ] + } } }, "info": { @@ -33,6 +327,279 @@ "type": "http" } }, - "schemas": {} + "schemas": { + "InvoiceListDto": { + "type": "object", + "properties": { + "invoices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "number": { + "type": "string", + "minLength": 1 + }, + "status": { + "type": "string", + "enum": [ + "Draft", + "Pending", + "Paid", + "Unpaid", + "Overdue", + "Cancelled", + "Refunded", + "Collections" + ] + }, + "currency": { + "type": "string", + "minLength": 1 + }, + "currencySymbol": { + "type": "string", + "minLength": 1 + }, + "total": { + "type": "number" + }, + "subtotal": { + "type": "number" + }, + "tax": { + "type": "number" + }, + "issuedAt": { + "type": "string" + }, + "dueDate": { + "type": "string" + }, + "paidDate": { + "type": "string" + }, + "pdfUrl": { + "type": "string" + }, + "paymentUrl": { + "type": "string" + }, + "description": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "description": { + "type": "string", + "minLength": 1 + }, + "amount": { + "type": "number" + }, + "quantity": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "type": { + "type": "string", + "minLength": 1 + }, + "serviceId": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": [ + "id", + "description", + "amount", + "type" + ] + } + }, + "daysOverdue": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": [ + "id", + "number", + "status", + "currency", + "total", + "subtotal", + "tax" + ] + } + }, + "pagination": { + "type": "object", + "properties": { + "page": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "totalPages": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "totalItems": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "nextCursor": { + "type": "string" + } + }, + "required": [ + "page", + "totalPages", + "totalItems" + ] + } + }, + "required": [ + "invoices", + "pagination" + ] + }, + "InvoiceDto": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "number": { + "type": "string", + "minLength": 1 + }, + "status": { + "type": "string", + "enum": [ + "Draft", + "Pending", + "Paid", + "Unpaid", + "Overdue", + "Cancelled", + "Refunded", + "Collections" + ] + }, + "currency": { + "type": "string", + "minLength": 1 + }, + "currencySymbol": { + "type": "string", + "minLength": 1 + }, + "total": { + "type": "number" + }, + "subtotal": { + "type": "number" + }, + "tax": { + "type": "number" + }, + "issuedAt": { + "type": "string" + }, + "dueDate": { + "type": "string" + }, + "paidDate": { + "type": "string" + }, + "pdfUrl": { + "type": "string" + }, + "paymentUrl": { + "type": "string" + }, + "description": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "description": { + "type": "string", + "minLength": 1 + }, + "amount": { + "type": "number" + }, + "quantity": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "type": { + "type": "string", + "minLength": 1 + }, + "serviceId": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": [ + "id", + "description", + "amount", + "type" + ] + } + }, + "daysOverdue": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": [ + "id", + "number", + "status", + "currency", + "total", + "subtotal", + "tax" + ] + } + } } } \ No newline at end of file diff --git a/apps/bff/scripts/openapi.module.ts b/apps/bff/scripts/openapi.module.ts index dc97b205..7cb3b165 100644 --- a/apps/bff/scripts/openapi.module.ts +++ b/apps/bff/scripts/openapi.module.ts @@ -2,9 +2,12 @@ import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { MinimalController } from "./minimal.controller"; +// Import controllers for OpenAPI generation +import { InvoicesController } from "../src/modules/invoices/invoices.controller"; + /** - * Minimal module for OpenAPI generation - * Only includes a basic controller with no dependencies + * OpenAPI generation module + * Includes all controllers but with minimal dependencies for schema generation */ @Module({ imports: [ @@ -26,6 +29,27 @@ import { MinimalController } from "./minimal.controller"; ], controllers: [ MinimalController, + InvoicesController, + ], + providers: [ + // Mock providers for controllers that need them + { + provide: "InvoicesOrchestratorService", + useValue: {}, + }, + { + provide: "WhmcsService", + useValue: {}, + }, + { + provide: "MappingsService", + useValue: {}, + }, + // Add other required services as mocks + { + provide: "Logger", + useValue: { log: () => {}, error: () => {}, warn: () => {} }, + }, ], }) export class OpenApiModule {} diff --git a/apps/bff/src/core/validation/index.ts b/apps/bff/src/core/validation/index.ts index 27a3bad7..332531a1 100644 --- a/apps/bff/src/core/validation/index.ts +++ b/apps/bff/src/core/validation/index.ts @@ -1,34 +1,15 @@ /** - * Validation Module Exports - * Direct Zod validation without separate validation package + * ✅ CLEAN Validation Module + * Consolidated validation patterns using nestjs-zod */ import { ZodValidationPipe, createZodDto } from "nestjs-zod"; -import type { ZodSchema } from "zod"; -import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from "@nestjs/common"; -// Re-export the proper ZodPipe from nestjs-zod +// ✅ RECOMMENDED: Only re-export what's needed export { ZodValidationPipe, createZodDto }; -// For use with @UsePipes() decorator - this creates a pipe instance -export function ZodPipe(schema: ZodSchema) { - return new ZodValidationPipe(schema); -} - -// For use with @Body() decorator - this creates a class factory -export function ZodPipeClass(schema: ZodSchema) { - @Injectable() - class ZodPipeClass implements PipeTransform { - transform(value: unknown, _metadata: ArgumentMetadata) { - const result = schema.safeParse(value); - if (!result.success) { - throw new BadRequestException({ - message: "Validation failed", - errors: result.error.issues, - }); - } - return result.data; - } - } - return ZodPipeClass; -} +// 📝 USAGE GUIDELINES: +// 1. For request validation: Use global ZodValidationPipe (configured in bootstrap.ts) +// 2. For DTOs: Use createZodDto(schema) for OpenAPI generation +// 3. For business logic: Use schema.safeParse() directly in services +// 4. For return types: Use domain types directly, not DTOs diff --git a/apps/bff/src/integrations/whmcs/transformers/services/invoice-transformer.service.ts b/apps/bff/src/integrations/whmcs/transformers/services/invoice-transformer.service.ts index fb6dabfd..f07f59bf 100644 --- a/apps/bff/src/integrations/whmcs/transformers/services/invoice-transformer.service.ts +++ b/apps/bff/src/integrations/whmcs/transformers/services/invoice-transformer.service.ts @@ -43,20 +43,30 @@ export class InvoiceTransformerService { defaultCurrency.prefix || defaultCurrency.suffix; + // Parse dates first to use in status determination + const dueDate = DataUtils.formatDate(whmcsInvoice.duedate); + const paidDate = DataUtils.formatDate(whmcsInvoice.datepaid); + const issuedAt = DataUtils.formatDate(whmcsInvoice.date || whmcsInvoice.datecreated); + + // Calculate days overdue if applicable + const finalStatus = StatusNormalizer.determineInvoiceStatus(whmcsInvoice.status, dueDate); + const daysOverdue = finalStatus === "Overdue" ? StatusNormalizer.calculateDaysOverdue(dueDate) : undefined; + const invoice: Invoice = { id: Number(invoiceId), number: whmcsInvoice.invoicenum || `INV-${invoiceId}`, - status: StatusNormalizer.normalizeInvoiceStatus(whmcsInvoice.status), + status: finalStatus, currency, currencySymbol, total: DataUtils.parseAmount(whmcsInvoice.total), subtotal: DataUtils.parseAmount(whmcsInvoice.subtotal), tax: DataUtils.parseAmount(whmcsInvoice.tax) + DataUtils.parseAmount(whmcsInvoice.tax2), - issuedAt: DataUtils.formatDate(whmcsInvoice.date || whmcsInvoice.datecreated), - dueDate: DataUtils.formatDate(whmcsInvoice.duedate), - paidDate: DataUtils.formatDate(whmcsInvoice.datepaid), + issuedAt, + dueDate, + paidDate, description: whmcsInvoice.notes || undefined, items: this.transformInvoiceItems(whmcsInvoice.items), + daysOverdue, }; if (!this.validator.validateInvoice(invoice)) { @@ -64,7 +74,11 @@ export class InvoiceTransformerService { } this.logger.debug(`Transformed invoice ${invoice.id}`, { - status: invoice.status, + originalStatus: whmcsInvoice.status, + finalStatus: invoice.status, + dueDate: invoice.dueDate, + isOverdue: StatusNormalizer.isInvoiceOverdue(invoice.dueDate), + daysOverdue: StatusNormalizer.calculateDaysOverdue(invoice.dueDate), total: invoice.total, currency: invoice.currency, itemCount: invoice.items?.length || 0, diff --git a/apps/bff/src/integrations/whmcs/transformers/services/payment-transformer.service.ts b/apps/bff/src/integrations/whmcs/transformers/services/payment-transformer.service.ts index c5c9712f..543bca75 100644 --- a/apps/bff/src/integrations/whmcs/transformers/services/payment-transformer.service.ts +++ b/apps/bff/src/integrations/whmcs/transformers/services/payment-transformer.service.ts @@ -57,12 +57,13 @@ export class PaymentTransformerService { }; // Add credit card specific fields - if (whmcsPayMethod.last_four) { - transformed.lastFour = whmcsPayMethod.last_four; + if (whmcsPayMethod.card_last_four) { + transformed.lastFour = whmcsPayMethod.card_last_four; } - if (whmcsPayMethod.cc_type) { - transformed.ccType = whmcsPayMethod.cc_type; + if (whmcsPayMethod.card_type) { + transformed.ccType = whmcsPayMethod.card_type; + transformed.cardBrand = whmcsPayMethod.card_type; } if (whmcsPayMethod.expiry_date) { diff --git a/apps/bff/src/integrations/whmcs/transformers/utils/status-normalizer.ts b/apps/bff/src/integrations/whmcs/transformers/utils/status-normalizer.ts index 900178dd..6fa926a8 100644 --- a/apps/bff/src/integrations/whmcs/transformers/utils/status-normalizer.ts +++ b/apps/bff/src/integrations/whmcs/transformers/utils/status-normalizer.ts @@ -26,6 +26,35 @@ export class StatusNormalizer { return statusMap[status?.toLowerCase()] || "Unpaid"; } + /** + * Determine the correct invoice status based on WHMCS status and due date + * This handles the case where WHMCS doesn't automatically update status to "Overdue" + */ + static determineInvoiceStatus(whmcsStatus: string, dueDate?: string): InvoiceStatus { + const normalizedStatus = this.normalizeInvoiceStatus(whmcsStatus); + + // If already marked as paid, cancelled, refunded, etc., keep that status + if (["Paid", "Cancelled", "Refunded", "Collections", "Draft", "Pending"].includes(normalizedStatus)) { + return normalizedStatus; + } + + // For unpaid invoices, check if they're actually overdue + if (normalizedStatus === "Unpaid" && dueDate) { + const dueDateObj = new Date(dueDate); + const today = new Date(); + + // Set time to start of day for accurate comparison + today.setHours(0, 0, 0, 0); + dueDateObj.setHours(0, 0, 0, 0); + + if (dueDateObj < today) { + return "Overdue"; + } + } + + return normalizedStatus; + } + /** * Normalize product status to our standard values */ @@ -93,4 +122,37 @@ export class StatusNormalizer { const pendingStatuses = ["pending", "draft", "payment pending"]; return pendingStatuses.includes(status?.toLowerCase()); } + + /** + * Check if an invoice is overdue based on due date + */ + static isInvoiceOverdue(dueDate?: string): boolean { + if (!dueDate) return false; + + const dueDateObj = new Date(dueDate); + const today = new Date(); + + // Set time to start of day for accurate comparison + today.setHours(0, 0, 0, 0); + dueDateObj.setHours(0, 0, 0, 0); + + return dueDateObj < today; + } + + /** + * Calculate days overdue for an invoice + */ + static calculateDaysOverdue(dueDate?: string): number { + if (!dueDate || !this.isInvoiceOverdue(dueDate)) return 0; + + const dueDateObj = new Date(dueDate); + const today = new Date(); + + // Set time to start of day for accurate comparison + today.setHours(0, 0, 0, 0); + dueDateObj.setHours(0, 0, 0, 0); + + const diffTime = today.getTime() - dueDateObj.getTime(); + return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + } } diff --git a/apps/bff/src/integrations/whmcs/types/whmcs-api.types.ts b/apps/bff/src/integrations/whmcs/types/whmcs-api.types.ts index a6ef2526..427ee25b 100644 --- a/apps/bff/src/integrations/whmcs/types/whmcs-api.types.ts +++ b/apps/bff/src/integrations/whmcs/types/whmcs-api.types.ts @@ -293,12 +293,12 @@ export interface WhmcsPaymentMethod { type: "CreditCard" | "BankAccount" | "RemoteCreditCard" | "RemoteBankAccount"; description: string; gateway_name?: string; - last_four?: string; + card_last_four?: string; expiry_date?: string; bank_name?: string; account_type?: string; remote_token?: string; - cc_type?: string; + card_type?: string; billing_contact_id?: number; created_at?: string; updated_at?: string; diff --git a/apps/bff/src/modules/invoices/invoices.controller.ts b/apps/bff/src/modules/invoices/invoices.controller.ts index ec8ccb66..2d91b303 100644 --- a/apps/bff/src/modules/invoices/invoices.controller.ts +++ b/apps/bff/src/modules/invoices/invoices.controller.ts @@ -19,6 +19,7 @@ import { ApiBearerAuth, ApiParam, } from "@nestjs/swagger"; +import { createZodDto } from "nestjs-zod"; import { InvoicesOrchestratorService } from "./services/invoices-orchestrator.service"; import { WhmcsService } from "@bff/integrations/whmcs/whmcs.service"; import { MappingsService } from "@bff/modules/id-mappings/mappings.service"; @@ -32,6 +33,14 @@ import type { PaymentGatewayList, InvoicePaymentLink, } from "@customer-portal/domain"; +import { + invoiceSchema, + invoiceListSchema, +} from "@customer-portal/domain/validation/shared/entities"; + +// ✅ CLEAN: DTOs only for OpenAPI generation +class InvoiceDto extends createZodDto(invoiceSchema) {} +class InvoiceListDto extends createZodDto(invoiceListSchema) {} interface AuthenticatedRequest { user: { id: string }; @@ -71,7 +80,10 @@ export class InvoicesController { type: String, description: "Filter by invoice status", }) - @ApiOkResponse({ description: "List of invoices with pagination" }) + @ApiOkResponse({ + description: "List of invoices with pagination", + type: InvoiceListDto + }) async getInvoices( @Request() req: AuthenticatedRequest, @Query("page") page?: string, @@ -152,7 +164,10 @@ export class InvoicesController { description: "Retrieves detailed information for a specific invoice", }) @ApiParam({ name: "id", type: Number, description: "Invoice ID" }) - @ApiOkResponse({ description: "Invoice details" }) + @ApiOkResponse({ + description: "Invoice details", + type: InvoiceDto + }) @ApiResponse({ status: 404, description: "Invoice not found" }) async getInvoiceById( @Request() req: AuthenticatedRequest, diff --git a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceSummaryBar.tsx b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceSummaryBar.tsx index 06e075bc..c2fc79a3 100644 --- a/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceSummaryBar.tsx +++ b/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceSummaryBar.tsx @@ -19,6 +19,11 @@ const statusVariantMap: Partial> = { @@ -28,6 +33,8 @@ const statusLabelMap: Partial> = { Refunded: "Refunded", Draft: "Draft", Cancelled: "Cancelled", + Pending: "Pending", + Collections: "Collections", }; function formatDisplayDate(dateString?: string) { @@ -37,17 +44,20 @@ function formatDisplayDate(dateString?: string) { return format(date, "dd MMM yyyy"); } -function formatRelativeDue(dateString: string | undefined, status: Invoice["status"]) { +function formatRelativeDue(dateString: string | undefined, status: Invoice["status"], daysOverdue?: number) { if (!dateString) return null; if (status === "Paid") return null; - const dueDate = new Date(dateString); - if (Number.isNaN(dueDate.getTime())) return null; + if (status === "Overdue" && daysOverdue) { + return `${daysOverdue} day${daysOverdue !== 1 ? 's' : ''} overdue`; + } else if (status === "Unpaid") { + const dueDate = new Date(dateString); + if (Number.isNaN(dueDate.getTime())) return null; + const distance = formatDistanceToNowStrict(dueDate); + return `due in ${distance}`; + } - const isOverdue = dueDate.getTime() < Date.now(); - const distance = formatDistanceToNowStrict(dueDate); - - return isOverdue ? `${distance} overdue` : `due in ${distance}`; + return null; } export function InvoiceSummaryBar({ @@ -69,8 +79,8 @@ export function InvoiceSummaryBar({ const dueDisplay = useMemo(() => formatDisplayDate(invoice.dueDate), [invoice.dueDate]); const issuedDisplay = useMemo(() => formatDisplayDate(invoice.issuedAt), [invoice.issuedAt]); const relativeDue = useMemo( - () => formatRelativeDue(invoice.dueDate, invoice.status), - [invoice.dueDate, invoice.status] + () => formatRelativeDue(invoice.dueDate, invoice.status, invoice.daysOverdue), + [invoice.dueDate, invoice.status, invoice.daysOverdue] ); const statusVariant = statusVariantMap[invoice.status] ?? "neutral"; @@ -144,10 +154,10 @@ export function InvoiceSummaryBar({ disabled={!onPay} loading={loadingPayment} rightIcon={} - variant={invoice.status === "Overdue" ? "destructive" : "default"} + variant="default" className="order-1 sm:order-2 lg:order-1" > - {invoice.status === "Overdue" ? "Pay Overdue" : "Pay Now"} + Pay Now )} diff --git a/apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx b/apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx index 98a14856..69139d38 100644 --- a/apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx +++ b/apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx @@ -1,15 +1,16 @@ "use client"; import React, { useMemo, useState } from "react"; +import { MagnifyingGlassIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { LoadingTable } from "@/components/atoms/loading-skeleton"; import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock"; -import { SearchFilterBar } from "@/components/molecules/SearchFilterBar/SearchFilterBar"; import { PaginationBar } from "@/components/molecules/PaginationBar/PaginationBar"; import { InvoiceTable } from "@/features/billing/components/InvoiceTable/InvoiceTable"; import { useInvoices } from "@/features/billing/hooks/useBilling"; import { useSubscriptionInvoices } from "@/features/subscriptions/hooks/useSubscriptions"; import type { Invoice } from "@customer-portal/domain"; +import { cn } from "@/lib/utils"; interface InvoicesListProps { subscriptionId?: number; @@ -87,36 +88,86 @@ export function InvoicesList({ } return ( - { - setStatusFilter(value); - setCurrentPage(1); - }} - filterOptions={isSubscriptionMode ? undefined : statusFilterOptions} - filterLabel={isSubscriptionMode ? undefined : "Filter by status"} +
+ {/* Clean Header */} + {showFilters && ( +
+
+ {/* Title Section */} +
+

+ Invoices +

+ {pagination?.totalItems && ( + + {pagination.totalItems} total + + )} +
+ + {/* Controls */} +
+ {/* Search Input */} +
+
+ +
+ setSearchTerm(e.target.value)} + /> +
+ + {/* Filter Dropdown */} + {!isSubscriptionMode && ( +
+ +
+ +
+
+ )} +
+
+
+ )} + + {/* Invoice Table */} +
+ - } - headerClassName="bg-gray-50 rounded md:p-2 p-1 mb-1" - footer={ - pagination && filtered.length > 0 ? ( - - ) : undefined - } - className={className} - > - - + {pagination && filtered.length > 0 && ( +
+ +
+ )} +
+
); } diff --git a/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx b/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx index a2f2911c..54ed2d63 100644 --- a/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx +++ b/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx @@ -1,8 +1,7 @@ "use client"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; -import Link from "next/link"; import { format } from "date-fns"; import { DocumentTextIcon, @@ -10,12 +9,19 @@ import { CheckCircleIcon, ExclamationTriangleIcon, ClockIcon, + CreditCardIcon, + ArrowDownTrayIcon, } from "@heroicons/react/24/outline"; +import { CheckCircleIcon as CheckCircleIconSolid } from "@heroicons/react/24/solid"; import { DataTable } from "@/components/molecules/DataTable/DataTable"; +import { Button } from "@/components/atoms/button"; import { BillingStatusBadge } from "../BillingStatusBadge"; import type { Invoice } from "@customer-portal/domain"; import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain"; import { cn } from "@/lib/utils"; +import { useCreateInvoiceSsoLink } from "@/features/billing/hooks/useBilling"; +import { openSsoLink } from "@/features/billing/utils/sso"; +import { logger } from "@customer-portal/logging"; interface InvoiceTableProps { invoices: Invoice[]; @@ -51,6 +57,9 @@ export function InvoiceTable({ className, }: InvoiceTableProps) { const router = useRouter(); + const [paymentLoading, setPaymentLoading] = useState(null); + const [downloadLoading, setDownloadLoading] = useState(null); + const createSsoLinkMutation = useCreateInvoiceSsoLink(); const handleInvoiceClick = (invoice: Invoice) => { if (onInvoiceClick) { @@ -60,88 +69,195 @@ export function InvoiceTable({ } }; + const handlePayment = async (invoice: Invoice, event: React.MouseEvent) => { + event.stopPropagation(); // Prevent row click + setPaymentLoading(invoice.id); + try { + const ssoLink = await createSsoLinkMutation.mutateAsync({ + invoiceId: invoice.id, + target: "pay" + }); + openSsoLink(ssoLink.url, { newTab: true }); + } catch (err) { + logger.error(err, "Failed to create payment SSO link"); + } finally { + setPaymentLoading(null); + } + }; + + const handleDownload = async (invoice: Invoice, event: React.MouseEvent) => { + event.stopPropagation(); // Prevent row click + setDownloadLoading(invoice.id); + try { + const ssoLink = await createSsoLinkMutation.mutateAsync({ + invoiceId: invoice.id, + target: "download" + }); + openSsoLink(ssoLink.url, { newTab: false }); + } catch (err) { + logger.error(err, "Failed to create download SSO link"); + } finally { + setDownloadLoading(null); + } + }; + const columns = useMemo(() => { const baseColumns = [ { key: "invoice", - header: "Invoice", - render: (invoice: Invoice) => ( -
- {getStatusIcon(invoice.status)} -
-
{invoice.number}
- {!compact && invoice.description && ( -
{invoice.description}
- )} + header: "Invoice Details", + className: "w-1/3", + render: (invoice: Invoice) => { + const statusIcon = getStatusIcon(invoice.status); + return ( +
+
+ {statusIcon} +
+
+
+ {invoice.number} +
+ {!compact && invoice.description && ( +
+ {invoice.description} +
+ )} + {!compact && invoice.issuedAt && ( +
+ Issued {format(new Date(invoice.issuedAt), "MMM d, yyyy")} +
+ )} +
-
- ), + ); + }, }, { key: "status", header: "Status", - render: (invoice: Invoice) => , + className: "w-36", + render: (invoice: Invoice) => { + const renderStatusWithDate = () => { + switch (invoice.status) { + case "Paid": + return ( +
+ + Paid + + {invoice.paidDate && ( +
+ {format(new Date(invoice.paidDate), "MMM d, yyyy")} +
+ )} +
+ ); + + case "Overdue": + return ( +
+ + Overdue + + {invoice.daysOverdue && ( +
+ {invoice.daysOverdue} day{invoice.daysOverdue !== 1 ? 's' : ''} overdue +
+ )} +
+ ); + + case "Unpaid": + return ( +
+ + Unpaid + + {invoice.dueDate && ( +
+ Due {format(new Date(invoice.dueDate), "MMM d, yyyy")} +
+ )} +
+ ); + + default: + // Use the existing BillingStatusBadge for other statuses + return ; + } + }; + + return ( +
+ {renderStatusWithDate()} +
+ ); + }, }, { key: "amount", header: "Amount", + className: "w-32 text-right", render: (invoice: Invoice) => ( - - {formatCurrency(invoice.total, { - currency: invoice.currency, - locale: getCurrencyLocale(invoice.currency), - })} - +
+
+ {formatCurrency(invoice.total, { + currency: invoice.currency, + locale: getCurrencyLocale(invoice.currency), + })} +
+
), }, ]; - // Add date columns if not compact - if (!compact) { - baseColumns.push( - { - key: "invoiceDate", - header: "Invoice Date", - render: (invoice: Invoice) => ( - - {invoice.issuedAt ? format(new Date(invoice.issuedAt), "MMM d, yyyy") : "N/A"} - - ), - }, - { - key: "dueDate", - header: "Due Date", - render: (invoice: Invoice) => ( - - {invoice.dueDate ? format(new Date(invoice.dueDate), "MMM d, yyyy") : "N/A"} - - ), - } - ); - } - // Add actions column if enabled if (showActions) { baseColumns.push({ key: "actions", - header: "", - render: (invoice: Invoice) => ( -
- e.stopPropagation()} - > - View - - -
- ), + header: "Actions", + className: "w-48 text-right", + render: (invoice: Invoice) => { + const canPay = invoice.status === "Unpaid" || invoice.status === "Overdue"; + const isPaymentLoading = paymentLoading === invoice.id; + const isDownloadLoading = downloadLoading === invoice.id; + + return ( +
+ {/* Payment Button - Only for unpaid invoices - Always on the left */} + {canPay && ( + + )} + + {/* Download Button - Always available and always on the right */} + +
+ ); + }, }); } return baseColumns; - }, [compact, showActions]); + }, [compact, showActions, paymentLoading, downloadLoading]); const emptyState = { icon: , @@ -151,24 +267,72 @@ export function InvoiceTable({ if (loading) { return ( -
-
- {Array.from({ length: 8 }).map((_, i) => ( -
- ))} +
+
+ {/* Header skeleton */} +
+
+
+
+
+
+
+
+ {/* Row skeletons */} +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
); } return ( - +
+ +
); } diff --git a/apps/portal/src/features/billing/components/PaymentMethodCard.tsx b/apps/portal/src/features/billing/components/PaymentMethodCard.tsx index 2d355b25..898a1839 100644 --- a/apps/portal/src/features/billing/components/PaymentMethodCard.tsx +++ b/apps/portal/src/features/billing/components/PaymentMethodCard.tsx @@ -1,6 +1,6 @@ "use client"; -import { CreditCardIcon, BanknotesIcon, DevicePhoneMobileIcon } from "@heroicons/react/24/outline"; +import { CreditCardIcon, BanknotesIcon, DevicePhoneMobileIcon, CheckCircleIcon } from "@heroicons/react/24/outline"; import type { PaymentMethod } from "@customer-portal/domain"; import { StatusPill } from "@/components/atoms/status-pill"; import { cn } from "@/lib/utils"; @@ -13,24 +13,73 @@ interface PaymentMethodCardProps { actionSlot?: ReactNode; } -const getMethodIcon = (type: PaymentMethod["type"], brand?: string) => { - if (type === "BankAccount" || type === "RemoteBankAccount") { - return ; - } - if (brand?.toLowerCase().includes("mobile")) { - return ; - } - return ; +const getBrandColor = (brand?: string) => { + const brandLower = brand?.toLowerCase() || ""; + + if (brandLower.includes("visa")) return "from-blue-600 to-blue-700"; + if (brandLower.includes("mastercard") || brandLower.includes("master")) return "from-red-500 to-red-600"; + if (brandLower.includes("amex") || brandLower.includes("american")) return "from-gray-700 to-gray-800"; + if (brandLower.includes("discover")) return "from-orange-500 to-orange-600"; + if (brandLower.includes("mobile")) return "from-purple-500 to-purple-600"; + + return "from-gray-500 to-gray-600"; // Default }; -const formatDescription = (method: PaymentMethod) => { - if (method.cardBrand && method.lastFour) { - return `${method.cardBrand.toUpperCase()} •••• ${method.lastFour}`; +const getMethodIcon = (type: PaymentMethod["type"], brand?: string) => { + const baseClasses = "w-12 h-12 bg-gradient-to-br rounded-xl flex items-center justify-center shadow-sm"; + + if (isBankAccount(type)) { + return ( +
+ +
+ ); } - if (method.type === "BankAccount" && method.lastFour) { - return `Bank Account •••• ${method.lastFour}`; + + const brandColor = getBrandColor(brand); + const IconComponent = brand?.toLowerCase().includes("mobile") ? DevicePhoneMobileIcon : CreditCardIcon; + + return ( +
+ +
+ ); +}; + +const isCreditCard = (type: PaymentMethod["type"]) => + type === "CreditCard" || type === "RemoteCreditCard"; + +const isBankAccount = (type: PaymentMethod["type"]) => + type === "BankAccount" || type === "RemoteBankAccount"; + +const formatCardDisplay = (method: PaymentMethod) => { + // Show ***** and last 4 digits for any payment method with lastFour + if (method.lastFour) { + return `***** ${method.lastFour}`; } - return method.description; + + // Fallback based on type + if (isCreditCard(method.type)) { + return method.cardBrand ? `${method.cardBrand.toUpperCase()} Card` : "Credit Card"; + } + + if (isBankAccount(method.type)) { + return method.bankName || "Bank Account"; + } + + return method.description || "Payment Method"; +}; + +const formatCardBrand = (method: PaymentMethod) => { + if (isCreditCard(method.type) && method.cardBrand) { + return method.cardBrand.toUpperCase(); + } + + if (isBankAccount(method.type) && method.bankName) { + return method.bankName; + } + + return null; }; const formatExpiry = (expiryDate?: string) => { @@ -44,34 +93,55 @@ export function PaymentMethodCard({ showActions = false, actionSlot, }: PaymentMethodCardProps) { - const description = formatDescription(paymentMethod); + const cardDisplay = formatCardDisplay(paymentMethod); + const cardBrand = formatCardBrand(paymentMethod); const expiry = formatExpiry(paymentMethod.expiryDate); const icon = getMethodIcon(paymentMethod.type, paymentMethod.cardBrand ?? paymentMethod.ccType); return (
-
+
{icon}
-
-
-

{description}

- {paymentMethod.isDefault ? ( - - ) : null} +
+
+

{cardDisplay}

+ {paymentMethod.isDefault && ( +
+ + Default +
+ )}
-
- {paymentMethod.gatewayDisplayName || paymentMethod.gatewayName || paymentMethod.type} + +
+ {cardBrand && ( + {cardBrand} + )} + {expiry && ( + <> + {cardBrand && } + {expiry} + + )}
- {expiry ?
{expiry}
: null} + + {paymentMethod.isDefault && ( +
+ This card will be used for automatic payments +
+ )}
- {showActions && actionSlot ?
{actionSlot}
: null} + {showActions && actionSlot && ( +
{actionSlot}
+ )}
); } diff --git a/apps/portal/src/features/billing/hooks/useBilling.ts b/apps/portal/src/features/billing/hooks/useBilling.ts index 3ec13d3e..a2b054b5 100644 --- a/apps/portal/src/features/billing/hooks/useBilling.ts +++ b/apps/portal/src/features/billing/hooks/useBilling.ts @@ -136,3 +136,17 @@ export function useCreateInvoiceSsoLink( ...options, }); } + +export function useCreatePaymentMethodsSsoLink( + options?: UseMutationOptions +): UseMutationResult { + return useMutation({ + mutationFn: async () => { + const response = await apiClient.POST("/auth/sso-link", { + body: { destination: "index.php?rp=/account/paymentmethods" }, + }); + return getDataOrThrow(response, "Failed to create payment methods SSO link"); + }, + ...options, + }); +} diff --git a/apps/portal/src/features/billing/views/PaymentMethods.tsx b/apps/portal/src/features/billing/views/PaymentMethods.tsx index bdf3823b..c9d09a85 100644 --- a/apps/portal/src/features/billing/views/PaymentMethods.tsx +++ b/apps/portal/src/features/billing/views/PaymentMethods.tsx @@ -7,10 +7,10 @@ import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { useSession } from "@/features/auth/hooks"; import { useAuthStore } from "@/features/auth/services/auth.store"; -import { apiClient, getDataOrThrow, isApiError } from "@/lib/api"; +import { isApiError } from "@/lib/api"; import { openSsoLink } from "@/features/billing/utils/sso"; import { usePaymentRefresh } from "@/features/billing/hooks/usePaymentRefresh"; -import { PaymentMethodCard, usePaymentMethods } from "@/features/billing"; +import { PaymentMethodCard, usePaymentMethods, useCreatePaymentMethodsSsoLink } from "@/features/billing"; import { CreditCardIcon, PlusIcon } from "@heroicons/react/24/outline"; import { InlineToast } from "@/components/atoms/inline-toast"; import { SectionHeader } from "@/components/molecules"; @@ -19,10 +19,8 @@ import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock"; import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton"; import { logger } from "@customer-portal/logging"; import { EmptyState } from "@/components/atoms/empty-state"; -import type { InvoiceSsoLink } from "@customer-portal/domain"; export function PaymentMethodsContainer() { - const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const { isAuthenticated } = useSession(); @@ -34,6 +32,8 @@ export function PaymentMethodsContainer() { error: paymentMethodsError, } = paymentMethodsQuery; + const createPaymentMethodsSsoLink = useCreatePaymentMethodsSsoLink(); + // Auth hydration flag to avoid showing empty state before auth is checked const { hasCheckedAuth } = useAuthStore(); @@ -47,21 +47,16 @@ export function PaymentMethodsContainer() { }); const openPaymentMethods = async () => { - setIsLoading(true); - setError(null); - if (!isAuthenticated) { setError("Please log in to access payment methods."); - setIsLoading(false); return; } + setError(null); + try { - const response = await apiClient.POST("/auth/sso-link", { - body: { path: "index.php?rp=/account/paymentmethods" }, - }); - const sso = getDataOrThrow(response, "Failed to open payment methods portal"); - openSsoLink(sso.url, { newTab: true }); + const ssoLink = await createPaymentMethodsSsoLink.mutateAsync(); + openSsoLink(ssoLink.url, { newTab: true }); } catch (err: unknown) { logger.error(err, "Failed to open payment methods"); if (isApiError(err) && err.response.status === 401) { @@ -69,8 +64,6 @@ export function PaymentMethodsContainer() { } else { setError("Unable to access payment methods. Please try again later."); } - } finally { - setIsLoading(false); } }; @@ -104,7 +97,7 @@ export function PaymentMethodsContainer() { } title="Payment Methods" - description="Manage your payment methods in the billing portal." + description="Manage your saved payment methods and billing information" > - {/* Simplified: remove verbose banner; controls exist via buttons */} -
-
+ +
+
{!hasCheckedAuth || isLoadingPaymentMethods || isFetchingPaymentMethods ? ( - <> - - +
+
+
+
+ + +
+ +
{Array.from({ length: 2 }).map((_, i) => ( -
-
- -
- - +
+
+
+ +
+ + +
+
-
))}
- - - ) : paymentMethodsData && paymentMethodsData.paymentMethods.length > 0 ? ( - - - - } - > -
- {paymentMethodsData.paymentMethods.map(paymentMethod => ( - - ))}
-
+
+ ) : paymentMethodsData && paymentMethodsData.paymentMethods.length > 0 ? ( +
+
+
+
+

Your Payment Methods

+

+ {paymentMethodsData.paymentMethods.length} payment method{paymentMethodsData.paymentMethods.length !== 1 ? 's' : ''} on file +

+
+
+ +

+ Opens in a new tab for security +

+
+
+
+ +
+
+ {paymentMethodsData.paymentMethods.map((paymentMethod) => ( + + ))} +
+
+
) : ( - +
{!hasCheckedAuth && !paymentMethodsData ? ( - - <> - +
+ + <> + +
) : ( - <> - } - title="No Payment Methods" - description="Open the billing portal to add a card." - action={{ - label: isLoading ? "Opening..." : "Manage Cards", - onClick: () => void openPaymentMethods(), - }} - /> -

- Opens in a new tab for security +

+
+ +
+

No Payment Methods

+

+ Open the billing portal to add a card.

- +
+ +

+ Opens in a new tab for security +

+
+
)} - +
)}
-
-
-
-
- -
-
-

Secure & Encrypted

-

- All payment information is securely encrypted and protected with - industry-standard security. -

+
+
+
+
+
+ +
+
+

Secure & Encrypted

+

+ All payment information is securely encrypted and protected with + industry-standard security. +

+
-
-
-

Supported Payment Methods

-
    -
  • • Credit Cards (Visa, MasterCard, American Express)
  • -
  • • Debit Cards
  • -
+
+

Supported Payment Methods

+
    +
  • • Credit Cards (Visa, MasterCard, American Express)
  • +
  • • Debit Cards
  • +
+
@@ -218,4 +244,4 @@ export function PaymentMethodsContainer() { ); } -export default PaymentMethodsContainer; +export default PaymentMethodsContainer; \ No newline at end of file diff --git a/apps/portal/src/features/catalog/views/InternetPlans.tsx b/apps/portal/src/features/catalog/views/InternetPlans.tsx index 018c0685..55671812 100644 --- a/apps/portal/src/features/catalog/views/InternetPlans.tsx +++ b/apps/portal/src/features/catalog/views/InternetPlans.tsx @@ -106,9 +106,8 @@ export function InternetPlansContainer() {
-
- ); -} + ); + } return (
@@ -227,8 +226,9 @@ export function InternetPlansContainer() {
)} -
- +
+ +
); } diff --git a/apps/portal/src/lib/api/__generated__/types.ts b/apps/portal/src/lib/api/__generated__/types.ts index 679a77ce..debde5e6 100644 --- a/apps/portal/src/lib/api/__generated__/types.ts +++ b/apps/portal/src/lib/api/__generated__/types.ts @@ -4,2722 +4,475 @@ */ export interface paths { - "/api/orders": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/minimal": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Minimal endpoint for OpenAPI generation */ + get: operations["MinimalController_getMinimal"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - get?: never; - put?: never; - /** Create Salesforce Order */ - post: operations["OrdersController_create"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/orders/user": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/invoices": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get paginated list of user invoices + * @description Retrieves invoices for the authenticated user with pagination and optional status filtering + */ + get: operations["InvoicesController_getInvoices"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** Get user's orders */ - get: operations["OrdersController_getUserOrders"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/orders/{sfOrderId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/invoices/payment-methods": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get user payment methods + * @description Retrieves all saved payment methods for the authenticated user + */ + get: operations["InvoicesController_getPaymentMethods"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** Get order summary/status */ - get: operations["OrdersController_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/invoices/payment-gateways": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get available payment gateways + * @description Retrieves all active payment gateways available for payments + */ + get: operations["InvoicesController_getPaymentGateways"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** Get current user profile */ - get: operations["UsersController_getProfile"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** Update user profile */ - patch: operations["UsersController_updateProfile"]; - trace?: never; - }; - "/api/me/summary": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/invoices/payment-methods/refresh": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Refresh payment methods cache + * @description Invalidates and refreshes payment methods cache for the current user + */ + post: operations["InvoicesController_refreshPaymentMethods"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** Get user dashboard summary */ - get: operations["UsersController_getSummary"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/me/address": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/invoices/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get invoice details by ID + * @description Retrieves detailed information for a specific invoice + */ + get: operations["InvoicesController_getInvoiceById"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** Get mailing address */ - get: operations["UsersController_getAddress"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** Update mailing address */ - patch: operations["UsersController_updateAddress"]; - trace?: never; - }; - "/api/auth/validate-signup": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/invoices/{id}/subscriptions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get subscriptions related to an invoice + * @description Retrieves all subscriptions that are referenced in the invoice items + */ + get: operations["InvoicesController_getInvoiceSubscriptions"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - get?: never; - put?: never; - /** Validate customer number for signup */ - post: operations["AuthController_validateSignup"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/health-check": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/invoices/{id}/sso-link": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create SSO link for invoice + * @description Generates a single sign-on link to view/pay the invoice or download PDF in WHMCS + */ + post: operations["InvoicesController_createSsoLink"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** Check auth service health and integrations */ - get: operations["AuthController_healthCheck"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/signup-preflight": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/invoices/{id}/payment-link": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create payment link for invoice with payment method + * @description Generates a payment link for the invoice with a specific payment method or gateway + */ + post: operations["InvoicesController_createPaymentLink"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - get?: never; - put?: never; - /** Validate full signup data without creating anything */ - post: operations["AuthController_signupPreflight"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/account-status": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Get account status by email */ - post: operations["AuthController_accountStatus"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/signup": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Create new user account */ - post: operations["AuthController_signup"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/login": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Authenticate user */ - post: operations["AuthController_login"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/logout": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Logout user */ - post: operations["AuthController_logout"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/link-whmcs": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Link existing WHMCS user */ - post: operations["AuthController_linkWhmcs"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/set-password": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Set password for linked user */ - post: operations["AuthController_setPassword"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/check-password-needed": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Check if user needs to set password */ - post: operations["AuthController_checkPasswordNeeded"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/request-password-reset": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Request password reset email */ - post: operations["AuthController_requestPasswordReset"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/reset-password": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Reset password with token */ - post: operations["AuthController_resetPassword"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/change-password": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Change password (authenticated) */ - post: operations["AuthController_changePassword"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/me": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get current authentication status */ - get: operations["AuthController_getAuthStatus"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/sso-link": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Create SSO link to WHMCS */ - post: operations["AuthController_createSsoLink"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/admin/audit-logs": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get audit logs (admin only) */ - get: operations["AuthAdminController_getAuditLogs"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/admin/unlock-account/{userId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Unlock user account (admin only) */ - post: operations["AuthAdminController_unlockAccount"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/auth/admin/security-stats": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get security statistics (admin only) */ - get: operations["AuthAdminController_getSecurityStats"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/catalog/internet/plans": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get Internet plans filtered by customer eligibility */ - get: operations["CatalogController_getInternetPlans"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/catalog/internet/addons": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get Internet add-ons */ - get: operations["CatalogController_getInternetAddons"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/catalog/internet/installations": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get Internet installations */ - get: operations["CatalogController_getInternetInstallations"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/catalog/sim/plans": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get SIM plans filtered by user's existing services */ - get: operations["CatalogController_getSimPlans"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/catalog/sim/activation-fees": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get SIM activation fees */ - get: operations["CatalogController_getSimActivationFees"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/catalog/sim/addons": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get SIM add-ons */ - get: operations["CatalogController_getSimAddons"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/catalog/vpn/plans": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get VPN plans */ - get: operations["CatalogController_getVpnPlans"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/catalog/vpn/activation-fees": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get VPN activation fees */ - get: operations["CatalogController_getVpnActivationFees"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/invoices": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get paginated list of user invoices - * @description Retrieves invoices for the authenticated user with pagination and optional status filtering - */ - get: operations["InvoicesController_getInvoices"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/invoices/payment-methods": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get user payment methods - * @description Retrieves all saved payment methods for the authenticated user - */ - get: operations["InvoicesController_getPaymentMethods"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/invoices/payment-gateways": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get available payment gateways - * @description Retrieves all active payment gateways available for payments - */ - get: operations["InvoicesController_getPaymentGateways"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/invoices/test-payment-methods/{clientId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Test WHMCS payment methods API for specific client ID - * @description Direct test of WHMCS GetPayMethods API - TEMPORARY DEBUG ENDPOINT - */ - get: operations["InvoicesController_testPaymentMethods"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/invoices/payment-methods/refresh": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Refresh payment methods cache - * @description Invalidates and refreshes payment methods cache for the current user - */ - post: operations["InvoicesController_refreshPaymentMethods"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/invoices/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get invoice details by ID - * @description Retrieves detailed information for a specific invoice - */ - get: operations["InvoicesController_getInvoiceById"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/invoices/{id}/subscriptions": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get subscriptions related to an invoice - * @description Retrieves all subscriptions that are referenced in the invoice items - */ - get: operations["InvoicesController_getInvoiceSubscriptions"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/invoices/{id}/sso-link": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Create SSO link for invoice - * @description Generates a single sign-on link to view/pay the invoice or download PDF in WHMCS - */ - post: operations["InvoicesController_createSsoLink"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/invoices/{id}/payment-link": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Create payment link for invoice with payment method - * @description Generates a payment link for the invoice with a specific payment method or gateway - */ - post: operations["InvoicesController_createPaymentLink"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get all user subscriptions - * @description Retrieves all subscriptions/services for the authenticated user - */ - get: operations["SubscriptionsController_getSubscriptions"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/active": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get active subscriptions only - * @description Retrieves only active subscriptions for the authenticated user - */ - get: operations["SubscriptionsController_getActiveSubscriptions"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/stats": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get subscription statistics - * @description Retrieves subscription count statistics by status - */ - get: operations["SubscriptionsController_getSubscriptionStats"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/{id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get subscription details by ID - * @description Retrieves detailed information for a specific subscription - */ - get: operations["SubscriptionsController_getSubscriptionById"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/{id}/invoices": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get invoices for a specific subscription - * @description Retrieves all invoices related to a specific subscription - */ - get: operations["SubscriptionsController_getSubscriptionInvoices"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/{id}/sim/debug": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Debug SIM subscription data - * @description Retrieves subscription data to help debug SIM management issues - */ - get: operations["SubscriptionsController_debugSimSubscription"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/{id}/sim": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get SIM details and usage - * @description Retrieves comprehensive SIM information including details and current usage - */ - get: operations["SubscriptionsController_getSimInfo"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/{id}/sim/details": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get SIM details - * @description Retrieves detailed SIM information including ICCID, plan, status, etc. - */ - get: operations["SubscriptionsController_getSimDetails"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/{id}/sim/usage": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get SIM data usage - * @description Retrieves current data usage and recent usage history - */ - get: operations["SubscriptionsController_getSimUsage"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/{id}/sim/top-up-history": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get SIM top-up history - * @description Retrieves data top-up history for the specified date range - */ - get: operations["SubscriptionsController_getSimTopUpHistory"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/{id}/sim/top-up": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Top up SIM data quota - * @description Add data quota to the SIM service - */ - post: operations["SubscriptionsController_topUpSim"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/{id}/sim/change-plan": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Change SIM plan - * @description Change the SIM service plan. The change will be automatically scheduled for the 1st of the next month. - */ - post: operations["SubscriptionsController_changeSimPlan"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/{id}/sim/cancel": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Cancel SIM service - * @description Cancel the SIM service (immediate or scheduled) - */ - post: operations["SubscriptionsController_cancelSim"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/{id}/sim/reissue-esim": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Reissue eSIM profile - * @description Reissue a downloadable eSIM profile (eSIM only). Optionally provide a new EID to transfer to. - */ - post: operations["SubscriptionsController_reissueEsimProfile"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/{id}/sim/features": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Update SIM features - * @description Enable/disable voicemail, call waiting, international roaming, and switch network type (4G/5G) - */ - post: operations["SubscriptionsController_updateSimFeatures"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/subscriptions/sim/orders/activate": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Create invoice, capture payment, and activate SIM in Freebit */ - post: operations["SimOrdersController_activate"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/health": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["HealthController_getHealth"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; } export type webhooks = Record; export interface components { - schemas: { - CreateOrderDto: Record; - UpdateUserDto: { - /** @description User's first name */ - firstName?: string; - /** @description User's last name */ - lastName?: string; - /** @description User's company name */ - company?: string; - /** @description User's phone number */ - phone?: string; - /** @description User's email address */ - email?: string; + schemas: { + InvoiceListDto: { + invoices: { + id: number; + number: string; + /** @enum {string} */ + status: "Draft" | "Pending" | "Paid" | "Unpaid" | "Overdue" | "Cancelled" | "Refunded" | "Collections"; + currency: string; + currencySymbol?: string; + total: number; + subtotal: number; + tax: number; + issuedAt?: string; + dueDate?: string; + paidDate?: string; + pdfUrl?: string; + paymentUrl?: string; + description?: string; + items?: { + id: number; + description: string; + amount: number; + quantity?: number; + type: string; + serviceId?: number; + }[]; + daysOverdue?: number; + }[]; + pagination: { + page: number; + totalPages: number; + totalItems: number; + nextCursor?: string; + }; + }; + InvoiceDto: { + id: number; + number: string; + /** @enum {string} */ + status: "Draft" | "Pending" | "Paid" | "Unpaid" | "Overdue" | "Cancelled" | "Refunded" | "Collections"; + currency: string; + currencySymbol?: string; + total: number; + subtotal: number; + tax: number; + issuedAt?: string; + dueDate?: string; + paidDate?: string; + pdfUrl?: string; + paymentUrl?: string; + description?: string; + items?: { + id: number; + description: string; + amount: number; + quantity?: number; + type: string; + serviceId?: number; + }[]; + daysOverdue?: number; + }; }; - UpdateAddressDto: { - /** @description Street address */ - street?: string; - /** @description Street address line 2 */ - streetLine2?: string; - /** @description City */ - city?: string; - /** @description State/Prefecture */ - state?: string; - /** @description Postal code */ - postalCode?: string; - /** @description Country (ISO alpha-2) */ - country?: string; - }; - ValidateSignupDto: { - /** - * @description Customer Number (SF Number) to validate - * @example 12345 - */ - sfNumber: string; - }; - AddressDto: { - /** @example 123 Main Street */ - street: string; - /** @example Apt 4B */ - streetLine2?: string; - /** @example Tokyo */ - city: string; - /** @example Tokyo */ - state: string; - /** @example 100-0001 */ - postalCode: string; - /** - * @description ISO 2-letter country code - * @example JP - */ - country: string; - }; - SignupDto: { - /** @example user@example.com */ - email: string; - /** - * @description Password must be at least 8 characters and contain uppercase, lowercase, number, and special character - * @example SecurePassword123! - */ - password: string; - /** @example John */ - firstName: string; - /** @example Doe */ - lastName: string; - /** @example Acme Corp */ - company?: string; - /** - * @description Contact phone number - * @example +81-90-1234-5678 - */ - phone: string; - /** - * @description Customer Number (SF Number) - * @example CN-0012345 - */ - sfNumber: string; - /** @description Address for WHMCS client (required) */ - address: components["schemas"]["AddressDto"]; - nationality?: string; - /** @example 1990-01-01 */ - dateOfBirth?: string; - /** @enum {string} */ - gender?: "male" | "female" | "other"; - }; - AccountStatusRequestDto: { - /** @example user@example.com */ - email: string; - }; - LinkWhmcsDto: { - /** @example user@example.com */ - email: string; - /** @example existing-whmcs-password */ - password: string; - }; - SetPasswordDto: { - /** @example user@example.com */ - email: string; - /** - * @description Password must be at least 8 characters and contain uppercase, lowercase, number, and special character - * @example NewSecurePassword123! - */ - password: string; - }; - RequestPasswordResetDto: { - /** @example user@example.com */ - email: string; - }; - ResetPasswordDto: { - /** @description Password reset token */ - token: string; - /** @example SecurePassword123! */ - password: string; - }; - ChangePasswordDto: { - /** @example CurrentPassword123! */ - currentPassword: string; - /** @example NewSecurePassword123! */ - newPassword: string; - }; - SsoLinkDto: { - /** - * @description WHMCS destination path - * @example index.php?rp=/account/paymentmethods - */ - destination?: string; - }; - InvoiceItemDto: { - /** @example 101 */ - id: number; - /** @example Monthly hosting */ - description: string; - /** @example 19.99 */ - amount: number; - /** @example 1 */ - quantity?: number; - /** @example Hosting */ - type: string; - /** @example 555 */ - serviceId?: number; - }; - InvoiceDto: { - /** @example 1234 */ - id: number; - /** @example INV-2025-0001 */ - number: string; - /** @example Unpaid */ - status: string; - /** @example USD */ - currency: string; - /** @example ¥ */ - currencySymbol?: string; - /** @example 19.99 */ - total: number; - /** @example 18.17 */ - subtotal: number; - /** @example 1.82 */ - tax: number; - /** @example 2025-01-01T00:00:00.000Z */ - issuedAt?: string; - /** @example 2025-01-15T00:00:00.000Z */ - dueDate?: string; - /** @example 2025-01-10T00:00:00.000Z */ - paidDate?: string; - pdfUrl?: string; - paymentUrl?: string; - description?: string; - items?: components["schemas"]["InvoiceItemDto"][]; - }; - PaginationDto: { - /** @example 1 */ - page: number; - /** @example 5 */ - totalPages: number; - /** @example 42 */ - totalItems: number; - nextCursor?: string; - }; - InvoiceListDto: { - invoices: components["schemas"]["InvoiceDto"][]; - pagination: components["schemas"]["PaginationDto"]; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record; export interface operations { - OrdersController_create: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["CreateOrderDto"]; - }; - }; - responses: { - /** @description Order created successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid request data */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - OrdersController_getUserOrders: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - OrdersController_get: { - parameters: { - query?: never; - header?: never; - path: { - sfOrderId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - UsersController_getProfile: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description User profile retrieved successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - UsersController_updateProfile: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateUserDto"]; - }; - }; - responses: { - /** @description Profile updated successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid input data */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - UsersController_getSummary: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description User summary retrieved successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - UsersController_getAddress: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Address retrieved successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - UsersController_updateAddress: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UpdateAddressDto"]; - }; - }; - responses: { - /** @description Address updated successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid input data */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_validateSignup: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ValidateSignupDto"]; - }; - }; - responses: { - /** @description Validation successful */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Customer number not found */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Customer already has account */ - 409: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Too many validation attempts */ - 429: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_healthCheck: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Health check results */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_signupPreflight: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SignupDto"]; - }; - }; - responses: { - /** @description Preflight results with next action guidance */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_accountStatus: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["AccountStatusRequestDto"]; - }; - }; - responses: { - /** @description Account status */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_signup: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SignupDto"]; - }; - }; - responses: { - /** @description User created successfully */ - 201: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description User already exists */ - 409: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Too many signup attempts */ - 429: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_login: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Login successful */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid credentials */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_logout: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Logout successful */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_linkWhmcs: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["LinkWhmcsDto"]; - }; - }; - responses: { - /** @description WHMCS account linked successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invalid WHMCS credentials */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Too many link attempts */ - 429: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_setPassword: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SetPasswordDto"]; - }; - }; - responses: { - /** @description Password set successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description User not found */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Too many password attempts */ - 429: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_checkPasswordNeeded: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Password status checked */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_requestPasswordReset: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["RequestPasswordResetDto"]; - }; - }; - responses: { - /** @description Reset email sent if account exists */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_resetPassword: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ResetPasswordDto"]; - }; - }; - responses: { - /** @description Password reset successful */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_changePassword: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["ChangePasswordDto"]; - }; - }; - responses: { - /** @description Password changed successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_getAuthStatus: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_createSsoLink: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SsoLinkDto"]; - }; - }; - responses: { - /** @description SSO link created successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description User not found or not linked to WHMCS */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthAdminController_getAuditLogs: { - parameters: { - query: { - page: string; - limit: string; - userId: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Audit logs retrieved */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthAdminController_unlockAccount: { - parameters: { - query?: never; - header?: never; - path: { - userId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Account unlocked */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthAdminController_getSecurityStats: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Security stats retrieved */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - CatalogController_getInternetPlans: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - CatalogController_getInternetAddons: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - CatalogController_getInternetInstallations: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - CatalogController_getSimPlans: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - CatalogController_getSimActivationFees: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - CatalogController_getSimAddons: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - CatalogController_getVpnPlans: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - CatalogController_getVpnActivationFees: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - InvoicesController_getInvoices: { - parameters: { - query?: { - /** @description Page number (default: 1) */ - page?: number; - /** @description Items per page (default: 10) */ - limit?: number; - /** @description Filter by invoice status */ - status?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of invoices with pagination */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["InvoiceListDto"]; - }; - }; - }; - }; - InvoicesController_getPaymentMethods: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of payment methods */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - InvoicesController_getPaymentGateways: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of payment gateways */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - InvoicesController_testPaymentMethods: { - parameters: { - query?: never; - header?: never; - path: { - /** @description WHMCS Client ID to test */ - clientId: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - InvoicesController_refreshPaymentMethods: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Payment methods cache refreshed */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - InvoicesController_getInvoiceById: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Invoice ID */ - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Invoice details */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["InvoiceDto"]; - }; - }; - /** @description Invoice not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - InvoicesController_getInvoiceSubscriptions: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Invoice ID */ - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of related subscriptions */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invoice not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - InvoicesController_createSsoLink: { - parameters: { - query?: { - /** @description Link target: view invoice, download PDF, or go to payment page (default: view) */ - target?: "view" | "download" | "pay"; - }; - header?: never; - path: { - /** @description Invoice ID */ - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description SSO link created successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invoice not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - InvoicesController_createPaymentLink: { - parameters: { - query?: { - /** @description Payment method ID */ - paymentMethodId?: number; - /** @description Payment gateway name */ - gatewayName?: string; - }; - header?: never; - path: { - /** @description Invoice ID */ - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Payment link created successfully */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Invoice not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_getSubscriptions: { - parameters: { - query?: { - /** @description Filter by subscription status */ - status?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of user subscriptions */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_getActiveSubscriptions: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of active subscriptions */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_getSubscriptionStats: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Subscription statistics */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_getSubscriptionById: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Subscription ID */ - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Subscription details */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Subscription not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_getSubscriptionInvoices: { - parameters: { - query?: { - /** @description Page number (default: 1) */ - page?: number; - /** @description Items per page (default: 10) */ - limit?: number; - }; - header?: never; - path: { - /** @description Subscription ID */ - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of invoices for the subscription */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Subscription not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_debugSimSubscription: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Subscription ID */ - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Subscription debug data */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_getSimInfo: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Subscription ID */ - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description SIM information */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Not a SIM subscription */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Subscription not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_getSimDetails: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Subscription ID */ - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description SIM details */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_getSimUsage: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Subscription ID */ - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description SIM usage data */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_getSimTopUpHistory: { - parameters: { - query: { - /** @description Start date (YYYYMMDD) */ - fromDate: string; - /** @description End date (YYYYMMDD) */ - toDate: string; - }; - header?: never; - path: { - /** @description Subscription ID */ - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Top-up history */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_topUpSim: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Subscription ID */ - id: number; - }; - cookie?: never; - }; - /** @description Top-up request */ - requestBody: { - content: { - "application/json": { - /** - * @description Quota in MB - * @example 1000 - */ - quotaMb: number; - }; - }; - }; - responses: { - /** @description Top-up successful */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_changeSimPlan: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Subscription ID */ - id: number; - }; - cookie?: never; - }; - /** @description Plan change request */ - requestBody: { - content: { - "application/json": { - /** - * @description New plan code - * @example LTE3G_P01 - */ - newPlanCode: string; - }; - }; - }; - responses: { - /** @description Plan change successful */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_cancelSim: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Subscription ID */ - id: number; - }; - cookie?: never; - }; - /** @description Cancellation request */ - requestBody?: { - content: { - "application/json": { - /** - * @description Schedule cancellation (YYYYMMDD) - * @example 20241231 - */ - scheduledAt?: string; - }; - }; - }; - responses: { - /** @description Cancellation successful */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_reissueEsimProfile: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Subscription ID */ - id: number; - }; - cookie?: never; - }; - /** @description Optional new EID to transfer the eSIM to */ - requestBody: { - content: { - "application/json": { - /** - * @description 32-digit EID - * @example 89049032000001000000043598005455 - */ - newEid?: string; - }; - }; - }; - responses: { - /** @description eSIM reissue successful */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Not an eSIM subscription */ - 400: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SubscriptionsController_updateSimFeatures: { - parameters: { - query?: never; - header?: never; - path: { - /** @description Subscription ID */ - id: number; - }; - cookie?: never; - }; - /** @description Features update request */ - requestBody: { - content: { - "application/json": { - voiceMailEnabled?: boolean; - callWaitingEnabled?: boolean; - internationalRoamingEnabled?: boolean; - /** @enum {string} */ - networkType?: "4G" | "5G"; - }; - }; - }; - responses: { - /** @description Features update successful */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SimOrdersController_activate: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** @description SIM activation order payload */ - requestBody: { - content: { - "application/json": string; - }; - }; - responses: { - /** @description Activation processed */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - HealthController_getHealth: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; + MinimalController_getMinimal: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + InvoicesController_getInvoices: { + parameters: { + query?: { + /** @description Filter by invoice status */ + status?: string; + /** @description Items per page (default: 10) */ + limit?: number; + /** @description Page number (default: 1) */ + page?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of invoices with pagination */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["InvoiceListDto"]; + }; + }; + }; + }; + InvoicesController_getPaymentMethods: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of payment methods */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + InvoicesController_getPaymentGateways: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of payment gateways */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + InvoicesController_refreshPaymentMethods: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Payment methods cache refreshed */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + InvoicesController_getInvoiceById: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Invoice ID */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Invoice details */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["InvoiceDto"]; + }; + }; + /** @description Invoice not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + InvoicesController_getInvoiceSubscriptions: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Invoice ID */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of related subscriptions */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invoice not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + InvoicesController_createSsoLink: { + parameters: { + query?: { + /** @description Link target: view invoice, download PDF, or go to payment page (default: view) */ + target?: "view" | "download" | "pay"; + }; + header?: never; + path: { + /** @description Invoice ID */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description SSO link created successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invoice not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + InvoicesController_createPaymentLink: { + parameters: { + query?: { + /** @description Payment gateway name */ + gatewayName?: string; + /** @description Payment method ID */ + paymentMethodId?: number; + }; + header?: never; + path: { + /** @description Invoice ID */ + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Payment link created successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invoice not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; }; - content?: never; - }; }; - }; } diff --git a/package.json b/package.json index 715891f5..c02ad313 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,9 @@ "dev:watch": "pnpm --parallel --filter @customer-portal/domain --filter @customer-portal/portal --filter @customer-portal/bff run dev", "plesk:images": "bash ./scripts/plesk/build-images.sh", "openapi:gen": "pnpm --filter @customer-portal/bff run openapi:gen", - "codegen": "echo 'API types are auto-generated from OpenAPI spec in portal/lib/api'", - "postinstall": "pnpm openapi:gen && pnpm codegen || true" + "types:gen": "./scripts/generate-frontend-types.sh", + "codegen": "pnpm types:gen", + "postinstall": "pnpm codegen || true" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", diff --git a/packages/domain/src/validation/shared/entities.ts b/packages/domain/src/validation/shared/entities.ts index dc8c99c9..53fa1ef6 100644 --- a/packages/domain/src/validation/shared/entities.ts +++ b/packages/domain/src/validation/shared/entities.ts @@ -176,6 +176,7 @@ export const invoiceSchema = whmcsEntitySchema.extend({ paymentUrl: z.string().optional(), description: z.string().optional(), items: z.array(invoiceItemSchema).optional(), + daysOverdue: z.number().int().nonnegative().optional(), }); export const invoiceListSchema = z.object({ diff --git a/scripts/generate-frontend-types.sh b/scripts/generate-frontend-types.sh new file mode 100755 index 00000000..705f8fd1 --- /dev/null +++ b/scripts/generate-frontend-types.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# 🎯 Automated Frontend Type Generation +# This script ensures frontend types are always in sync with backend OpenAPI spec + +set -e + +echo "🔄 Generating OpenAPI spec from backend..." +cd "$(dirname "$0")/.." +pnpm openapi:gen + +echo "🔄 Generating frontend types from OpenAPI spec..." +npx openapi-typescript apps/bff/openapi/openapi.json -o apps/portal/src/lib/api/__generated__/types.ts + +echo "✅ Frontend types updated successfully!" +echo "📝 Updated: apps/portal/src/lib/api/__generated__/types.ts"