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(),