From 0a387275ff0d884972581c1311e4097324fc6a13 Mon Sep 17 00:00:00 2001 From: "T. Narantuya" Date: Fri, 29 Aug 2025 13:26:57 +0900 Subject: [PATCH] Refactor address handling in AuthService and SignupDto, and enhance order processing with address verification - Updated AuthService to directly access address fields and added support for address line 2. - Introduced AddressDto in SignupDto for structured address validation. - Modified OrdersController to utilize CreateOrderDto for improved type safety. - Enhanced OrderBuilder to include address snapshot functionality during order creation. - Updated UsersService to handle address updates and added new methods in WHMCS service for client updates. - Improved address confirmation logic in AddressConfirmation component for internet orders. --- apps/bff/src/auth/auth.service.ts | 11 +- apps/bff/src/auth/dto/signup.dto.ts | 58 ++++-- apps/bff/src/auth/guards/global-auth.guard.ts | 10 +- apps/bff/src/common/config/field-map.ts | 28 +-- apps/bff/src/orders/orders.controller.ts | 9 +- apps/bff/src/orders/orders.module.ts | 3 +- .../orders/services/order-builder.service.ts | 83 ++++++++- .../services/order-item-builder.service.ts | 2 +- .../services/order-orchestrator.service.ts | 9 +- .../services/order-validator.service.ts | 4 +- apps/bff/src/users/users.service.ts | 53 +++++- .../whmcs/services/whmcs-client.service.ts | 23 +++ .../services/whmcs-connection.service.ts | 10 ++ .../vendors/whmcs/types/whmcs-api.types.ts | 1 + apps/bff/src/vendors/whmcs/whmcs.service.ts | 10 ++ apps/portal/src/app/account/billing/page.tsx | 88 ++++++++-- .../app/catalog/internet/configure/page.tsx | 18 +- apps/portal/src/app/catalog/internet/page.tsx | 2 - .../src/app/catalog/sim/configure/page.tsx | 6 +- apps/portal/src/app/checkout/page.tsx | 18 +- apps/portal/src/app/dashboard/page.tsx | 4 +- apps/portal/src/app/orders/[id]/page.tsx | 8 +- apps/portal/src/app/orders/page.tsx | 49 +++--- .../components/catalog/activation-form.tsx | 2 +- .../src/components/catalog/mnp-form.tsx | 2 +- .../checkout/address-confirmation.tsx | 166 +++++++++++++----- .../guards/profile-completion-guard.tsx | 78 ++++++++ .../src/hooks/use-profile-completion.ts | 61 +++++++ 28 files changed, 637 insertions(+), 179 deletions(-) create mode 100644 apps/portal/src/components/guards/profile-completion-guard.tsx create mode 100644 apps/portal/src/hooks/use-profile-completion.ts diff --git a/apps/bff/src/auth/auth.service.ts b/apps/bff/src/auth/auth.service.ts index f4605ce3..8197f284 100644 --- a/apps/bff/src/auth/auth.service.ts +++ b/apps/bff/src/auth/auth.service.ts @@ -127,11 +127,12 @@ export class AuthService { email, companyname: company || "", phonenumber: phone || "", - address1: address?.line1, - city: address?.city, - state: address?.state, - postcode: address?.postalCode, - country: address?.country, + address1: address.line1, + address2: address.line2 || "", + city: address.city, + state: address.state, + postcode: address.postalCode, + country: address.country, password2: password, // WHMCS requires plain password for new clients customfields, }); diff --git a/apps/bff/src/auth/dto/signup.dto.ts b/apps/bff/src/auth/dto/signup.dto.ts index eb9ade93..6f960add 100644 --- a/apps/bff/src/auth/dto/signup.dto.ts +++ b/apps/bff/src/auth/dto/signup.dto.ts @@ -1,5 +1,47 @@ -import { IsEmail, IsString, MinLength, IsOptional, Matches, IsIn } from "class-validator"; +import { + IsEmail, + IsString, + MinLength, + IsOptional, + Matches, + IsIn, + ValidateNested, + IsNotEmpty, +} from "class-validator"; import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; + +export class AddressDto { + @ApiProperty({ example: "123 Main Street" }) + @IsString() + @IsNotEmpty() + line1: string; + + @ApiProperty({ example: "Apt 4B", required: false }) + @IsOptional() + @IsString() + line2?: string; + + @ApiProperty({ example: "Tokyo" }) + @IsString() + @IsNotEmpty() + city: string; + + @ApiProperty({ example: "Tokyo" }) + @IsString() + @IsNotEmpty() + state: string; + + @ApiProperty({ example: "100-0001" }) + @IsString() + @IsNotEmpty() + postalCode: string; + + @ApiProperty({ example: "JP", description: "ISO 2-letter country code" }) + @IsString() + @IsNotEmpty() + country: string; +} export class SignupDto { @ApiProperty({ example: "user@example.com" }) @@ -41,16 +83,10 @@ export class SignupDto { @IsString() sfNumber: string; - @ApiProperty({ required: false, description: "Address for WHMCS client" }) - @IsOptional() - address?: { - line1: string; - line2?: string; - city: string; - state: string; - postalCode: string; - country: string; // ISO 2-letter - }; + @ApiProperty({ description: "Address for WHMCS client (required)" }) + @ValidateNested() + @Type(() => AddressDto) + address: AddressDto; @ApiProperty({ required: false }) @IsOptional() diff --git a/apps/bff/src/auth/guards/global-auth.guard.ts b/apps/bff/src/auth/guards/global-auth.guard.ts index 2c9b30b1..85eb66ef 100644 --- a/apps/bff/src/auth/guards/global-auth.guard.ts +++ b/apps/bff/src/auth/guards/global-auth.guard.ts @@ -8,7 +8,7 @@ import { import { Reflector } from "@nestjs/core"; import { AuthGuard } from "@nestjs/passport"; import { ExtractJwt } from "passport-jwt"; -import { Request } from "express"; + import { TokenBlacklistService } from "../services/token-blacklist.service"; import { IS_PUBLIC_KEY } from "../decorators/public.decorator"; @@ -24,8 +24,12 @@ export class GlobalAuthGuard extends AuthGuard("jwt") implements CanActivate { } async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const route = `${request.method} ${request.route?.path || request.url}`; + const request = context.switchToHttp().getRequest<{ + method: string; + url: string; + route?: { path?: string }; + }>(); + const route = `${request.method} ${request.route?.path ?? request.url}`; // Check if the route is marked as public const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ diff --git a/apps/bff/src/common/config/field-map.ts b/apps/bff/src/common/config/field-map.ts index f105c676..ecc2344f 100644 --- a/apps/bff/src/common/config/field-map.ts +++ b/apps/bff/src/common/config/field-map.ts @@ -72,17 +72,11 @@ export type SalesforceFieldMap = { // WHMCS integration whmcsOrderId: string; - // Billing/Shipping snapshot fields + // Address fields + addressChanged: string; + + // Billing address snapshot fields billing: { - contactId: string; - street: string; - city: string; - state: string; - postalCode: string; - country: string; - }; - shipping: { - contactId: string; street: string; city: string; state: string; @@ -177,23 +171,17 @@ export function getSalesforceFieldMap(): SalesforceFieldMap { // WHMCS integration whmcsOrderId: process.env.ORDER_WHMCS_ORDER_ID_FIELD || "WHMCS_Order_ID__c", - // Billing/Shipping snapshot fields + // Address fields + addressChanged: process.env.ORDER_ADDRESS_CHANGED_FIELD || "Address_Changed__c", + + // Billing address snapshot fields billing: { - contactId: process.env.ORDER_BILL_TO_CONTACT_ID_FIELD || "BillToContactId", street: process.env.ORDER_BILL_TO_STREET_FIELD || "BillToStreet", city: process.env.ORDER_BILL_TO_CITY_FIELD || "BillToCity", state: process.env.ORDER_BILL_TO_STATE_FIELD || "BillToState", postalCode: process.env.ORDER_BILL_TO_POSTAL_CODE_FIELD || "BillToPostalCode", country: process.env.ORDER_BILL_TO_COUNTRY_FIELD || "BillToCountry", }, - shipping: { - contactId: process.env.ORDER_SHIP_TO_CONTACT_ID_FIELD || "ShipToContactId", - street: process.env.ORDER_SHIP_TO_STREET_FIELD || "ShipToStreet", - city: process.env.ORDER_SHIP_TO_CITY_FIELD || "ShipToCity", - state: process.env.ORDER_SHIP_TO_STATE_FIELD || "ShipToState", - postalCode: process.env.ORDER_SHIP_TO_POSTAL_CODE_FIELD || "ShipToPostalCode", - country: process.env.ORDER_SHIP_TO_COUNTRY_FIELD || "ShipToCountry", - }, }, orderItem: { billingCycle: process.env.ORDER_ITEM_BILLING_CYCLE_FIELD || "Billing_Cycle__c", diff --git a/apps/bff/src/orders/orders.controller.ts b/apps/bff/src/orders/orders.controller.ts index 33063702..648db600 100644 --- a/apps/bff/src/orders/orders.controller.ts +++ b/apps/bff/src/orders/orders.controller.ts @@ -3,6 +3,7 @@ import { OrderOrchestrator } from "./services/order-orchestrator.service"; import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from "@nestjs/swagger"; import { RequestWithUser } from "../auth/auth.types"; import { Logger } from "nestjs-pino"; +import { CreateOrderDto } from "./dto/order.dto"; @ApiTags("orders") @Controller("orders") @@ -17,12 +18,12 @@ export class OrdersController { @ApiOperation({ summary: "Create Salesforce Order" }) @ApiResponse({ status: 201, description: "Order created successfully" }) @ApiResponse({ status: 400, description: "Invalid request data" }) - async create(@Request() req: RequestWithUser, @Body() body: unknown) { + async create(@Request() req: RequestWithUser, @Body() body: CreateOrderDto) { this.logger.log( { userId: req.user?.id, - orderType: (body as any)?.orderType, - skuCount: (body as any)?.skus?.length || 0, + orderType: body.orderType, + skuCount: body.skus?.length || 0, }, "Order creation request received" ); @@ -34,7 +35,7 @@ export class OrdersController { { error: error instanceof Error ? error.message : String(error), userId: req.user?.id, - orderType: (body as any)?.orderType, + orderType: body.orderType, }, "Order creation failed" ); diff --git a/apps/bff/src/orders/orders.module.ts b/apps/bff/src/orders/orders.module.ts index 02037fd6..bde4b196 100644 --- a/apps/bff/src/orders/orders.module.ts +++ b/apps/bff/src/orders/orders.module.ts @@ -2,6 +2,7 @@ import { Module } from "@nestjs/common"; import { OrdersController } from "./orders.controller"; import { VendorsModule } from "../vendors/vendors.module"; import { MappingsModule } from "../mappings/mappings.module"; +import { UsersModule } from "../users/users.module"; // Clean modular order services import { OrderValidator } from "./services/order-validator.service"; @@ -10,7 +11,7 @@ import { OrderItemBuilder } from "./services/order-item-builder.service"; import { OrderOrchestrator } from "./services/order-orchestrator.service"; @Module({ - imports: [VendorsModule, MappingsModule], + imports: [VendorsModule, MappingsModule, UsersModule], controllers: [OrdersController], providers: [ // Clean architecture only diff --git a/apps/bff/src/orders/services/order-builder.service.ts b/apps/bff/src/orders/services/order-builder.service.ts index 53a7554b..205b6419 100644 --- a/apps/bff/src/orders/services/order-builder.service.ts +++ b/apps/bff/src/orders/services/order-builder.service.ts @@ -1,20 +1,28 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, Inject } from "@nestjs/common"; +import { Logger } from "nestjs-pino"; import { CreateOrderBody, UserMapping } from "../dto/order.dto"; import { getSalesforceFieldMap } from "../../common/config/field-map"; +import { UsersService } from "../../users/users.service"; /** * Handles building order header data from selections */ @Injectable() export class OrderBuilder { + constructor( + @Inject(Logger) private readonly logger: Logger, + private readonly usersService: UsersService + ) {} + /** * Build order fields for Salesforce Order creation */ - buildOrderFields( + async buildOrderFields( body: CreateOrderBody, userMapping: UserMapping, - pricebookId: string - ): Record { + pricebookId: string, + userId: string + ): Promise> { const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD const fields = getSalesforceFieldMap(); @@ -43,9 +51,8 @@ export class OrderBuilder { break; } - // Add address snapshot from WHMCS (authoritative source) - // Note: We'll need to pass userId separately or get it from the userMapping - // For now, skip address snapshot until we have proper user ID access + // Add address snapshot (single address for both billing and shipping) + await this.addAddressSnapshot(orderFields, userId, body); return orderFields; } @@ -131,8 +138,68 @@ export class OrderBuilder { } } - private addVpnFields(orderFields: Record, body: CreateOrderBody): void { + private addVpnFields(_orderFields: Record, _body: CreateOrderBody): void { // Note: Removed vpnRegion field - can be derived from service product metadata in OrderItems // VPN orders only need user configuration choices (none currently defined) } + + /** + * Add address snapshot to order + * Always captures current address in billing fields and flags if changed + */ + private async addAddressSnapshot( + orderFields: Record, + userId: string, + body: CreateOrderBody + ): Promise { + try { + const fields = getSalesforceFieldMap(); + const billingInfo = await this.usersService.getBillingInfo(userId); + + // Check if address was provided/updated in the order request + const orderAddress = (body.configurations as Record)?.address as + | Record + | undefined; + const addressChanged = !!orderAddress; + + // Use order address if provided, otherwise use current WHMCS address + const addressToUse = orderAddress || billingInfo.address; + + // Always populate billing address fields (even if empty) + // Combine street and streetLine2 for Salesforce BillToStreet field + const street = typeof addressToUse?.street === "string" ? addressToUse.street : ""; + const streetLine2 = + typeof addressToUse?.streetLine2 === "string" ? addressToUse.streetLine2 : ""; + const fullStreet = [street, streetLine2].filter(Boolean).join(", "); + + orderFields[fields.order.billing.street] = fullStreet || ""; + orderFields[fields.order.billing.city] = + typeof addressToUse?.city === "string" ? addressToUse.city : ""; + orderFields[fields.order.billing.state] = + typeof addressToUse?.state === "string" ? addressToUse.state : ""; + orderFields[fields.order.billing.postalCode] = + typeof addressToUse?.postalCode === "string" ? addressToUse.postalCode : ""; + orderFields[fields.order.billing.country] = + typeof addressToUse?.country === "string" ? addressToUse.country : ""; + + // Set Address_Changed flag if customer updated address during checkout + orderFields[fields.order.addressChanged] = addressChanged; + + if (addressChanged) { + this.logger.log({ userId }, "Customer updated address during checkout"); + } + + this.logger.debug( + { + userId, + hasAddress: !!street, + addressChanged, + }, + "Address snapshot added to order" + ); + } catch (error) { + this.logger.error({ userId, error }, "Failed to add address snapshot to order"); + // Don't fail the order creation, but log the issue + } + } } diff --git a/apps/bff/src/orders/services/order-item-builder.service.ts b/apps/bff/src/orders/services/order-item-builder.service.ts index 3be5ea9b..3bd3d980 100644 --- a/apps/bff/src/orders/services/order-item-builder.service.ts +++ b/apps/bff/src/orders/services/order-item-builder.service.ts @@ -139,7 +139,7 @@ export class OrderItemBuilder { { sku, found: !!res.records?.length, - hasPrice: !!(res.records?.[0] as any)?.UnitPrice, + hasPrice: !!(res.records?.[0] as { UnitPrice?: number })?.UnitPrice, }, "PricebookEntry query result" ); diff --git a/apps/bff/src/orders/services/order-orchestrator.service.ts b/apps/bff/src/orders/services/order-orchestrator.service.ts index ace6ee46..ff5fde6c 100644 --- a/apps/bff/src/orders/services/order-orchestrator.service.ts +++ b/apps/bff/src/orders/services/order-orchestrator.service.ts @@ -43,8 +43,13 @@ export class OrderOrchestrator { "Order validation completed successfully" ); - // 2) Build order fields (simplified - no address snapshot for now) - const orderFields = this.orderBuilder.buildOrderFields(validatedBody, userMapping, pricebookId); + // 2) Build order fields (includes address snapshot) + const orderFields = await this.orderBuilder.buildOrderFields( + validatedBody, + userMapping, + pricebookId, + userId + ); this.logger.log({ orderType: orderFields.Type }, "Creating Salesforce Order"); diff --git a/apps/bff/src/orders/services/order-validator.service.ts b/apps/bff/src/orders/services/order-validator.service.ts index 7e57ab8c..3ee0603e 100644 --- a/apps/bff/src/orders/services/order-validator.service.ts +++ b/apps/bff/src/orders/services/order-validator.service.ts @@ -28,8 +28,8 @@ export class OrderValidator { this.logger.debug( { bodyType: typeof rawBody, - hasOrderType: !!(rawBody as any)?.orderType, - hasSkus: !!(rawBody as any)?.skus, + hasOrderType: !!(rawBody as Record)?.orderType, + hasSkus: !!(rawBody as Record)?.skus, }, "Starting request format validation" ); diff --git a/apps/bff/src/users/users.service.ts b/apps/bff/src/users/users.service.ts index 2f47d3f1..2991b961 100644 --- a/apps/bff/src/users/users.service.ts +++ b/apps/bff/src/users/users.service.ts @@ -6,8 +6,10 @@ import { User, Activity } from "@customer-portal/shared"; import { User as PrismaUser } from "@prisma/client"; import { WhmcsService } from "../vendors/whmcs/whmcs.service"; import { SalesforceService } from "../vendors/salesforce/salesforce.service"; +import { WhmcsClientResponse } from "../vendors/whmcs/types/whmcs-api.types"; // Removed unused import: getSalesforceFieldMap import { MappingsService } from "../mappings/mappings.service"; +import { UpdateBillingDto } from "./dto/update-billing.dto"; // Enhanced type definitions for better type safety export interface EnhancedUser extends Omit { @@ -574,7 +576,7 @@ export class UsersService { phone: clientDetails.phonenumber || null, address: { street: clientDetails.address1 || null, - streetLine2: null, // address2 not available in current WHMCS response + streetLine2: clientDetails.address2 || null, city: clientDetails.city || null, state: clientDetails.state || null, postalCode: clientDetails.postcode || null, @@ -598,11 +600,50 @@ export class UsersService { /** * Update billing information in WHMCS (authoritative source) - * TODO: Implement WHMCS client update functionality */ - async updateBillingInfo(userId: string, billingData: any) { - // For now, return current billing info since WHMCS update is not implemented - this.logger.warn(`Billing update requested for ${userId} but WHMCS update not implemented`); - throw new Error("Billing update functionality not yet implemented"); + async updateBillingInfo(userId: string, billingData: UpdateBillingDto): Promise { + try { + // Get user mapping + const mapping = await this.mappingsService.findByUserId(userId); + if (!mapping) { + throw new NotFoundException("User mapping not found"); + } + + // Prepare WHMCS update data + const whmcsUpdateData: Partial = {}; + + if (billingData.street !== undefined) { + whmcsUpdateData.address1 = billingData.street; + } + if (billingData.streetLine2 !== undefined) { + whmcsUpdateData.address2 = billingData.streetLine2; + } + if (billingData.city !== undefined) { + whmcsUpdateData.city = billingData.city; + } + if (billingData.state !== undefined) { + whmcsUpdateData.state = billingData.state; + } + if (billingData.postalCode !== undefined) { + whmcsUpdateData.postcode = billingData.postalCode; + } + if (billingData.country !== undefined) { + whmcsUpdateData.country = billingData.country; + } + if (billingData.phone !== undefined) { + whmcsUpdateData.phonenumber = billingData.phone; + } + if (billingData.company !== undefined) { + whmcsUpdateData.companyname = billingData.company; + } + + // Update in WHMCS + await this.whmcsService.updateClient(mapping.whmcsClientId, whmcsUpdateData); + + this.logger.log({ userId }, "Successfully updated billing information in WHMCS"); + } catch (error) { + this.logger.error({ userId, error }, "Failed to update billing information"); + throw error; + } } } diff --git a/apps/bff/src/vendors/whmcs/services/whmcs-client.service.ts b/apps/bff/src/vendors/whmcs/services/whmcs-client.service.ts index 2e35a85d..ef81465b 100644 --- a/apps/bff/src/vendors/whmcs/services/whmcs-client.service.ts +++ b/apps/bff/src/vendors/whmcs/services/whmcs-client.service.ts @@ -100,6 +100,29 @@ export class WhmcsClientService { } } + /** + * Update client details + */ + async updateClient( + clientId: number, + updateData: Partial + ): Promise { + try { + await this.connectionService.updateClient(clientId, updateData); + + // Invalidate cache after update + await this.cacheService.invalidateUserCache(clientId.toString()); + + this.logger.log(`Successfully updated WHMCS client ${clientId}`); + } catch (error: unknown) { + this.logger.error(`Failed to update WHMCS client ${clientId}`, { + error: getErrorMessage(error), + updateData, + }); + throw error; + } + } + /** * Add new client */ diff --git a/apps/bff/src/vendors/whmcs/services/whmcs-connection.service.ts b/apps/bff/src/vendors/whmcs/services/whmcs-connection.service.ts index 5cbc258b..85ca9e8a 100644 --- a/apps/bff/src/vendors/whmcs/services/whmcs-connection.service.ts +++ b/apps/bff/src/vendors/whmcs/services/whmcs-connection.service.ts @@ -291,6 +291,16 @@ export class WhmcsConnectionService { }); } + async updateClient( + clientId: number, + updateData: Partial + ): Promise<{ result: string }> { + return this.makeRequest<{ result: string }>("UpdateClient", { + clientid: clientId, + ...updateData, + }); + } + async validateLogin(params: WhmcsValidateLoginParams): Promise { return this.makeRequest("ValidateLogin", params); } diff --git a/apps/bff/src/vendors/whmcs/types/whmcs-api.types.ts b/apps/bff/src/vendors/whmcs/types/whmcs-api.types.ts index f19ec224..ded1794a 100644 --- a/apps/bff/src/vendors/whmcs/types/whmcs-api.types.ts +++ b/apps/bff/src/vendors/whmcs/types/whmcs-api.types.ts @@ -25,6 +25,7 @@ export interface WhmcsClientResponse { lastname: string; email: string; address1?: string; + address2?: string; city?: string; state?: string; postcode?: string; diff --git a/apps/bff/src/vendors/whmcs/whmcs.service.ts b/apps/bff/src/vendors/whmcs/whmcs.service.ts index da18ecef..4f727e3e 100644 --- a/apps/bff/src/vendors/whmcs/whmcs.service.ts +++ b/apps/bff/src/vendors/whmcs/whmcs.service.ts @@ -176,6 +176,16 @@ export class WhmcsService { return this.clientService.getClientDetailsByEmail(email); } + /** + * Update client details in WHMCS + */ + async updateClient( + clientId: number, + updateData: Partial + ): Promise { + return this.clientService.updateClient(clientId, updateData); + } + /** * Add new client */ diff --git a/apps/portal/src/app/account/billing/page.tsx b/apps/portal/src/app/account/billing/page.tsx index 69778710..d79ce042 100644 --- a/apps/portal/src/app/account/billing/page.tsx +++ b/apps/portal/src/app/account/billing/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect } from "react"; +import { useSearchParams } from "next/navigation"; import { DashboardLayout } from "@/components/layout/dashboard-layout"; import { authenticatedApi } from "@/lib/api"; import { @@ -30,6 +31,9 @@ interface BillingInfo { } export default function BillingPage() { + const searchParams = useSearchParams(); + const isCompletionFlow = searchParams.get("complete") === "true"; + const [billingInfo, setBillingInfo] = useState(null); const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(false); @@ -38,7 +42,7 @@ export default function BillingPage() { const [saving, setSaving] = useState(false); useEffect(() => { - fetchBillingInfo(); + void fetchBillingInfo(); }, []); const fetchBillingInfo = async () => { @@ -70,10 +74,45 @@ export default function BillingPage() { const handleSave = async () => { if (!editedAddress) return; + // Validate required fields + const isComplete = !!( + editedAddress.street?.trim() && + editedAddress.city?.trim() && + editedAddress.state?.trim() && + editedAddress.postalCode?.trim() && + editedAddress.country?.trim() + ); + + if (!isComplete) { + setError("Please fill in all required address fields"); + return; + } + try { setSaving(true); - // TODO: Implement when WHMCS update is available - setError("Address updates are not yet implemented. This feature is coming soon."); + setError(null); + + // Update address via API + await authenticatedApi.patch("/users/billing", { + street: editedAddress.street, + streetLine2: editedAddress.streetLine2, + city: editedAddress.city, + state: editedAddress.state, + postalCode: editedAddress.postalCode, + country: editedAddress.country, + }); + + // Update local state + if (billingInfo) { + setBillingInfo({ + ...billingInfo, + address: editedAddress, + isComplete: true, + }); + } + + setEditing(false); + setEditedAddress(null); } catch (err) { setError(err instanceof Error ? err.message : "Failed to update address"); } finally { @@ -107,9 +146,30 @@ export default function BillingPage() {
-

Billing & Address

+

+ {isCompletionFlow ? "Complete Your Profile" : "Billing & Address"} +

+ {isCompletionFlow && ( +
+
+
+ +
+
+

+ Profile Completion Required +

+

+ Please review and complete your address information to access all features and + enable service ordering. +

+
+
+
+ )} + {error && (
@@ -128,7 +188,7 @@ export default function BillingPage() {
-

Address Information

+

Service Address

{billingInfo?.isComplete && !editing && (
diff --git a/apps/portal/src/app/catalog/internet/configure/page.tsx b/apps/portal/src/app/catalog/internet/configure/page.tsx index 3bbaff48..4b486b01 100644 --- a/apps/portal/src/app/catalog/internet/configure/page.tsx +++ b/apps/portal/src/app/catalog/internet/configure/page.tsx @@ -2,17 +2,11 @@ import { useState, useEffect } from "react"; import { PageLayout } from "@/components/layout/page-layout"; -import { - ServerIcon, - CheckCircleIcon, - CurrencyYenIcon, - ArrowLeftIcon, - ArrowRightIcon, -} from "@heroicons/react/24/outline"; +import { ServerIcon, ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline"; import { useRouter, useSearchParams } from "next/navigation"; import { authenticatedApi } from "@/lib/api"; import { InternetPlan, InternetAddon, InternetInstallation } from "@/shared/types/catalog.types"; -import { OrderSummary } from "@/components/catalog/order-summary"; + import { AddonGroup } from "@/components/catalog/addon-group"; import { InstallationOptions } from "@/components/catalog/installation-options"; import { LoadingSpinner } from "@/components/catalog/loading-spinner"; @@ -116,9 +110,9 @@ function InternetConfigureContent() { return () => { mounted = false; }; - }, [planSku, router]); + }, [planSku, router, searchParams]); - const canContinue = plan && mode && installPlan; + // const canContinue = plan && mode && installPlan; const steps = [ { number: 1, title: "Service Details", completed: currentStep > 1 }, @@ -305,8 +299,8 @@ function InternetConfigureContent() { information from our tech team for details.

- * Will appear on the invoice as "Platinum Base Plan". Device subscriptions - will be added later. + * Will appear on the invoice as "Platinum Base Plan". Device + subscriptions will be added later.

diff --git a/apps/portal/src/app/catalog/internet/page.tsx b/apps/portal/src/app/catalog/internet/page.tsx index 3c009d7d..40ce9067 100644 --- a/apps/portal/src/app/catalog/internet/page.tsx +++ b/apps/portal/src/app/catalog/internet/page.tsx @@ -8,8 +8,6 @@ import { ArrowLeftIcon, ArrowRightIcon, WifiIcon, - CheckIcon, - ExclamationTriangleIcon, HomeIcon, BuildingOfficeIcon, } from "@heroicons/react/24/outline"; diff --git a/apps/portal/src/app/catalog/sim/configure/page.tsx b/apps/portal/src/app/catalog/sim/configure/page.tsx index 91cb9681..686600c9 100644 --- a/apps/portal/src/app/catalog/sim/configure/page.tsx +++ b/apps/portal/src/app/catalog/sim/configure/page.tsx @@ -8,15 +8,13 @@ import { ArrowRightIcon, DevicePhoneMobileIcon, ExclamationTriangleIcon, - InformationCircleIcon, UsersIcon, - CheckCircleIcon, } from "@heroicons/react/24/outline"; import { authenticatedApi } from "@/lib/api"; import Link from "next/link"; import { SimPlan, SimActivationFee, SimAddon } from "@/shared/types/catalog.types"; -import { OrderSummary } from "@/components/catalog/order-summary"; + import { AddonGroup } from "@/components/catalog/addon-group"; import { LoadingSpinner } from "@/components/catalog/loading-spinner"; import { AnimatedCard } from "@/components/catalog/animated-card"; @@ -165,7 +163,7 @@ function SimConfigureContent() { return () => { mounted = false; }; - }, [planId]); + }, [planId, searchParams]); const validateForm = (): boolean => { const newErrors: Record = {}; diff --git a/apps/portal/src/app/checkout/page.tsx b/apps/portal/src/app/checkout/page.tsx index 37f0f3fc..008cde93 100644 --- a/apps/portal/src/app/checkout/page.tsx +++ b/apps/portal/src/app/checkout/page.tsx @@ -206,12 +206,15 @@ function CheckoutContent() { if (selections.portingDateOfBirth) configurations.portingDateOfBirth = selections.portingDateOfBirth; + // Include address in configurations if it was updated during checkout + if (confirmedAddress) { + configurations.address = confirmedAddress; + } + const orderData = { orderType, skus: skus, ...(Object.keys(configurations).length > 0 && { configurations }), - // Include address if it was updated during checkout - ...(confirmedAddress && { address: confirmedAddress }), }; const response = await authenticatedApi.post<{ sfOrderId: string }>("/orders", orderData); @@ -283,6 +286,7 @@ function CheckoutContent() { {/* Order Submission Message */} @@ -292,16 +296,16 @@ function CheckoutContent() {

Submit Your Order for Review

- You've configured your service and reviewed all details. Your order will be submitted - for review and approval. + You've configured your service and reviewed all details. Your order will be + submitted for review and approval.

What happens next?

• Your order will be reviewed by our team

-

• We'll set up your services in our system

+

• We'll set up your services in our system

• Payment will be processed using your card on file

-

• You'll receive confirmation once everything is ready

+

• You'll receive confirmation once everything is ready

@@ -368,7 +372,7 @@ function CheckoutContent() {