From 7cefee4c75d7fc406f335f300bb7b27eb9820da6 Mon Sep 17 00:00:00 2001 From: barsa Date: Tue, 21 Oct 2025 16:30:52 +0900 Subject: [PATCH] Update .gitignore and enhance CSRF controller and WHMCS services - Added exceptions to .gitignore for the portal public directory to ensure proper asset management. - Marked CSRF token retrieval and refresh endpoints as public for improved access control. - Refactored WHMCS invoice service to enhance error logging and processing of invoice records. - Updated WHMCS raw types to introduce a common schema for invoices, improving data consistency and validation. - Enhanced Salesforce order item transformation to streamline billing cycle handling. --- .gitignore | 2 + .../security/controllers/csrf.controller.ts | 3 + .../salesforce/utils/order-query-builder.ts | 1 - .../whmcs/services/whmcs-invoice.service.ts | 7 +- .../public/assets/images/assist logo.png | Bin 0 -> 33386 bytes apps/portal/public/assets/images/logo.svg | 9 ++ .../domain/billing/providers/whmcs/mapper.ts | 24 +++++- .../billing/providers/whmcs/raw.types.ts | 79 +++++++++++------- .../orders/providers/salesforce/mapper.ts | 4 +- .../providers/whmcs/raw.types.ts | 71 +++++----------- 10 files changed, 111 insertions(+), 89 deletions(-) create mode 100644 apps/portal/public/assets/images/assist logo.png create mode 100644 apps/portal/public/assets/images/logo.svg 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 0000000000000000000000000000000000000000..2babb4d8dc97375a9ea61c23bdce15be3f188875 GIT binary patch literal 33386 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 Jqho*L{{cQ + + + + + + + + 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(),