diff --git a/.gitignore b/.gitignore index fb4ef803..e6c58aca 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,8 @@ dist/ # Gatsby files .cache/ public/ +!apps/portal/public/ +!apps/portal/public/** # Storybook build outputs .out/ diff --git a/apps/bff/src/core/security/controllers/csrf.controller.ts b/apps/bff/src/core/security/controllers/csrf.controller.ts index d0b91bed..11e3b2a8 100644 --- a/apps/bff/src/core/security/controllers/csrf.controller.ts +++ b/apps/bff/src/core/security/controllers/csrf.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Post, Req, Res, Inject } from "@nestjs/common"; import type { Request, Response } from "express"; import { Logger } from "nestjs-pino"; import { CsrfService } from "../services/csrf.service"; +import { Public } from "@bff/modules/auth/decorators/public.decorator"; type AuthenticatedRequest = Request & { user?: { id: string; sessionId?: string }; @@ -15,6 +16,7 @@ export class CsrfController { @Inject(Logger) private readonly logger: Logger ) {} + @Public() @Get("token") getCsrfToken(@Req() req: AuthenticatedRequest, @Res() res: Response) { const sessionId = req.user?.sessionId || this.extractSessionId(req) || undefined; @@ -46,6 +48,7 @@ export class CsrfController { }); } + @Public() @Post("refresh") refreshCsrfToken(@Req() req: AuthenticatedRequest, @Res() res: Response) { const sessionId = req.user?.sessionId || this.extractSessionId(req) || undefined; diff --git a/apps/bff/src/integrations/salesforce/utils/order-query-builder.ts b/apps/bff/src/integrations/salesforce/utils/order-query-builder.ts index de4a593a..9376f44a 100644 --- a/apps/bff/src/integrations/salesforce/utils/order-query-builder.ts +++ b/apps/bff/src/integrations/salesforce/utils/order-query-builder.ts @@ -60,7 +60,6 @@ export function buildOrderItemSelectFields( "UnitPrice", "TotalPrice", "PricebookEntry.Id", - "Billing_Cycle__c", "WHMCS_Service_ID__c", ]; diff --git a/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts b/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts index 99b430e8..a055245b 100644 --- a/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts +++ b/apps/bff/src/integrations/whmcs/services/whmcs-invoice.service.ts @@ -211,8 +211,10 @@ export class WhmcsInvoiceService { } satisfies InvoiceList; } + const invoiceRecords = response.invoices.invoice; + const invoices: Invoice[] = []; - for (const whmcsInvoice of response.invoices.invoice) { + for (const whmcsInvoice of invoiceRecords) { try { // Transform using domain mapper const defaultCurrency = this.currencyService.getDefaultCurrency(); @@ -223,8 +225,9 @@ export class WhmcsInvoiceService { const parsed = invoiceSchema.parse(transformed as unknown); invoices.push(parsed); } catch (error) { - this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, { + this.logger.error(`Failed to transform WHMCS invoice ${whmcsInvoice?.id ?? "unknown"}`, { error: getErrorMessage(error), + rawInvoice: whmcsInvoice, }); } } diff --git a/apps/portal/public/assets/images/assist logo.png b/apps/portal/public/assets/images/assist logo.png new file mode 100644 index 00000000..2babb4d8 Binary files /dev/null and b/apps/portal/public/assets/images/assist logo.png differ diff --git a/apps/portal/public/assets/images/logo.svg b/apps/portal/public/assets/images/logo.svg new file mode 100644 index 00000000..1205b759 --- /dev/null +++ b/apps/portal/public/assets/images/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/domain/billing/providers/whmcs/mapper.ts b/packages/domain/billing/providers/whmcs/mapper.ts index 2141dc82..c74f8572 100644 --- a/packages/domain/billing/providers/whmcs/mapper.ts +++ b/packages/domain/billing/providers/whmcs/mapper.ts @@ -9,6 +9,8 @@ import { invoiceSchema } from "../../schema"; import { type WhmcsInvoiceRaw, whmcsInvoiceRawSchema, + type WhmcsInvoiceListItem, + whmcsInvoiceListItemSchema, type WhmcsInvoiceItemsRaw, whmcsInvoiceItemsRawSchema, } from "./raw.types"; @@ -94,8 +96,15 @@ export function transformWhmcsInvoice( rawInvoice: unknown, options: TransformInvoiceOptions = {} ): Invoice { - // Validate raw data - const whmcsInvoice = whmcsInvoiceRawSchema.parse(rawInvoice); + const invoicePayload = + rawInvoice && typeof (rawInvoice as { invoiceid?: unknown }).invoiceid !== "undefined" + ? whmcsInvoiceRawSchema.parse(rawInvoice) + : normalizeListInvoice(rawInvoice); + + const whmcsInvoice = { + ...invoicePayload, + invoiceid: invoicePayload.invoiceid ?? invoicePayload.id, + }; const currency = whmcsInvoice.currencycode || options.defaultCurrencyCode || "JPY"; const currencySymbol = @@ -105,7 +114,7 @@ export function transformWhmcsInvoice( // Transform to domain model const invoice: Invoice = { - id: whmcsInvoice.invoiceid ?? whmcsInvoice.id ?? 0, + id: whmcsInvoice.invoiceid ?? 0, number: whmcsInvoice.invoicenum || `INV-${whmcsInvoice.invoiceid}`, status: mapStatus(whmcsInvoice.status), currency, @@ -133,3 +142,12 @@ export function transformWhmcsInvoices( ): Invoice[] { return rawInvoices.map(raw => transformWhmcsInvoice(raw, options)); } + +function normalizeListInvoice(rawInvoice: unknown): WhmcsInvoiceRaw & { id?: number } { + const listItem: WhmcsInvoiceListItem = whmcsInvoiceListItemSchema.parse(rawInvoice); + const invoiceid = listItem.invoiceid ?? listItem.id; + return { + ...listItem, + invoiceid, + }; +} diff --git a/packages/domain/billing/providers/whmcs/raw.types.ts b/packages/domain/billing/providers/whmcs/raw.types.ts index 5a4ba8dd..c5f99475 100644 --- a/packages/domain/billing/providers/whmcs/raw.types.ts +++ b/packages/domain/billing/providers/whmcs/raw.types.ts @@ -110,39 +110,59 @@ export const whmcsInvoiceItemsRawSchema = z.object({ export type WhmcsInvoiceItemsRaw = z.infer; -// Raw WHMCS Invoice -export const whmcsInvoiceRawSchema = z.object({ +const whmcsInvoiceCommonSchema = z + .object({ + invoicenum: z.string().optional(), + userid: z.number(), + date: z.string(), + duedate: z.string(), + subtotal: z.string(), + credit: z.string(), + tax: z.string(), + tax2: z.string(), + total: z.string(), + balance: z.string().optional(), + status: z.string(), + paymentmethod: z.string(), + notes: z.string().optional(), + ccgateway: z.boolean().optional(), + items: whmcsInvoiceItemsRawSchema.optional(), + transactions: z.unknown().optional(), + clientid: z.number().optional(), + datecreated: z.string().optional(), + paymentmethodname: z.string().optional(), + currencyprefix: z.string().optional(), + currencysuffix: z.string().optional(), + lastcaptureattempt: z.string().optional(), + last_capture_attempt: z.string().optional(), + datepaid: z.string().optional(), + date_refunded: z.string().optional(), + date_cancelled: z.string().optional(), + created_at: z.string().optional(), + updated_at: z.string().optional(), + taxrate: z.string().optional(), + taxrate2: z.string().optional(), + firstname: z.string().optional(), + lastname: z.string().optional(), + companyname: z.string().optional(), + currencycode: z.string().optional(), + }) + .passthrough(); + +export const whmcsInvoiceListItemSchema = whmcsInvoiceCommonSchema.extend({ + id: z.number(), + invoiceid: z.number().optional(), +}); + +// Raw WHMCS Invoice (detailed GetInvoice response) +export const whmcsInvoiceRawSchema = whmcsInvoiceCommonSchema.extend({ invoiceid: z.number(), - invoicenum: z.string(), - userid: z.number(), - date: z.string(), - duedate: z.string(), - subtotal: z.string(), - credit: z.string(), - tax: z.string(), - tax2: z.string(), - total: z.string(), - balance: z.string().optional(), - status: z.string(), - paymentmethod: z.string(), - notes: z.string().optional(), - ccgateway: z.boolean().optional(), - items: whmcsInvoiceItemsRawSchema.optional(), - transactions: z.unknown().optional(), id: z.number().optional(), - clientid: z.number().optional(), - datecreated: z.string().optional(), - paymentmethodname: z.string().optional(), - currencycode: z.string().optional(), - currencyprefix: z.string().optional(), - currencysuffix: z.string().optional(), - lastcaptureattempt: z.string().optional(), - taxrate: z.string().optional(), - taxrate2: z.string().optional(), - datepaid: z.string().optional(), + balance: z.string().optional(), }); export type WhmcsInvoiceRaw = z.infer; +export type WhmcsInvoiceListItem = z.infer; // ============================================================================ // WHMCS Invoice List Response (GetInvoices API) @@ -153,7 +173,7 @@ export type WhmcsInvoiceRaw = z.infer; */ export const whmcsInvoiceListResponseSchema = z.object({ invoices: z.object({ - invoice: z.array(whmcsInvoiceRawSchema), + invoice: z.array(whmcsInvoiceListItemSchema), }), totalresults: z.number(), numreturned: z.number(), @@ -264,4 +284,3 @@ export const whmcsCurrenciesResponseSchema = z.object({ }).catchall(z.string().or(z.number())); export type WhmcsCurrenciesResponse = z.infer; - diff --git a/packages/domain/orders/providers/salesforce/mapper.ts b/packages/domain/orders/providers/salesforce/mapper.ts index c597b742..f8ab535d 100644 --- a/packages/domain/orders/providers/salesforce/mapper.ts +++ b/packages/domain/orders/providers/salesforce/mapper.ts @@ -25,6 +25,8 @@ export function transformSalesforceOrderItem( // PricebookEntry is unknown to avoid circular dependencies between domains const pricebookEntry = record.PricebookEntry as Record | null | undefined; const product = pricebookEntry?.Product2 as Record | undefined; + const productBillingCycle = product?.Billing_Cycle__c ?? undefined; + const billingCycle = record.Billing_Cycle__c ?? productBillingCycle ?? undefined; const details = orderItemDetailsSchema.parse({ id: record.Id, @@ -32,7 +34,7 @@ export function transformSalesforceOrderItem( quantity: normalizeQuantity(record.Quantity), unitPrice: coerceNumber(record.UnitPrice), totalPrice: coerceNumber(record.TotalPrice), - billingCycle: record.Billing_Cycle__c ?? undefined, + billingCycle, product: product ? { id: product.Id ?? undefined, diff --git a/packages/domain/subscriptions/providers/whmcs/raw.types.ts b/packages/domain/subscriptions/providers/whmcs/raw.types.ts index d5d0cc9b..a3218453 100644 --- a/packages/domain/subscriptions/providers/whmcs/raw.types.ts +++ b/packages/domain/subscriptions/providers/whmcs/raw.types.ts @@ -8,39 +8,6 @@ import { z } from "zod"; -const normalizeRequiredNumber = z.preprocess( - value => { - if (typeof value === "number") return value; - if (typeof value === "string" && value.trim().length > 0) { - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : value; - } - return value; - }, - z.number() -); - -const normalizeOptionalNumber = z.preprocess( - value => { - if (value === undefined || value === null || value === "") return undefined; - if (typeof value === "number") return value; - if (typeof value === "string") { - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : undefined; - } - return undefined; - }, - z.number().optional() -); - -const normalizeOptionalString = z.preprocess( - value => { - if (value === undefined || value === null || value === "") return undefined; - return String(value); - }, - z.string().optional() -); - // ============================================================================ // Request Parameter Types // ============================================================================ @@ -77,12 +44,12 @@ export const whmcsCustomFieldsContainerSchema = z.object({ // Raw WHMCS Product/Service (Subscription) export const whmcsProductRawSchema = z.object({ - id: normalizeRequiredNumber, - clientid: normalizeRequiredNumber, - serviceid: normalizeOptionalNumber, - pid: normalizeOptionalNumber, - orderid: normalizeOptionalNumber, - ordernumber: normalizeOptionalString, + id: z.number(), + clientid: z.number(), + serviceid: z.number().optional(), + pid: z.number().optional(), + orderid: z.number().optional(), + ordernumber: z.string().optional(), regdate: z.string(), name: z.string(), translated_name: z.string().optional(), @@ -90,12 +57,12 @@ export const whmcsProductRawSchema = z.object({ translated_groupname: z.string().optional(), domain: z.string().optional(), dedicatedip: z.string().optional(), - serverid: normalizeOptionalNumber, + serverid: z.number().optional(), servername: z.string().optional(), serverip: z.string().optional(), serverhostname: z.string().optional(), suspensionreason: z.string().optional(), - promoid: normalizeOptionalNumber, + promoid: z.number().optional(), subscriptionid: z.string().optional(), // Pricing @@ -117,10 +84,10 @@ export const whmcsProductRawSchema = z.object({ // Notes notes: z.string().optional(), - diskusage: normalizeOptionalNumber, - disklimit: normalizeOptionalNumber, - bwusage: normalizeOptionalNumber, - bwlimit: normalizeOptionalNumber, + diskusage: z.number().optional(), + disklimit: z.number().optional(), + bwusage: z.number().optional(), + bwlimit: z.number().optional(), lastupdate: z.string().optional(), // Custom fields @@ -148,13 +115,13 @@ export type WhmcsCustomField = z.infer; export const whmcsProductListResponseSchema = z.object({ result: z.enum(["success", "error"]).optional(), message: z.string().optional(), - clientid: z.union([z.number(), z.string()]).optional(), - serviceid: z.union([z.number(), z.string(), z.null()]).optional(), - pid: z.union([z.number(), z.string(), z.null()]).optional(), - domain: z.string().nullable().optional(), - totalresults: z.union([z.number(), z.string()]).optional(), - startnumber: normalizeOptionalNumber, - numreturned: normalizeOptionalNumber, + clientid: z.number().optional(), + serviceid: z.number().optional(), + pid: z.number().optional(), + domain: z.string().optional(), + totalresults: z.number().optional(), + startnumber: z.number().optional(), + numreturned: z.number().optional(), products: z.object({ product: z.union([whmcsProductRawSchema, z.array(whmcsProductRawSchema)]).optional(), }).optional(),