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"