Refactor code for consistency and readability across multiple files

- Standardized JSON and URL-encoded body parser configurations in main.ts.
- Simplified method signatures in order-builder.service.ts and order-orchestrator.service.ts.
- Enhanced logging format in order-item-builder.service.ts and order-validator.service.ts.
- Cleaned up whitespace and formatting in various components and services for improved readability.
- Updated Docker commands in manage.sh to use the new syntax.
This commit is contained in:
T. Narantuya 2025-08-28 18:27:11 +09:00
parent 2eb7cc6314
commit d00c47f41e
19 changed files with 381 additions and 269 deletions

View File

@ -53,14 +53,12 @@ async function bootstrap() {
}
// Configure JSON body parser with proper limits
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// Enhanced cookie parser with security options
app.use(cookieParser());
// Trust proxy configuration for reverse proxies
if (configService.get("TRUST_PROXY", "false") === "true") {
const httpAdapter = app.getHttpAdapter();

View File

@ -63,10 +63,7 @@ export class OrderBuilder {
orderFields[fields.order.activationStatus] = "Not Started";
}
private addInternetFields(
orderFields: Record<string, unknown>,
body: CreateOrderBody
): void {
private addInternetFields(orderFields: Record<string, unknown>, body: CreateOrderBody): void {
const fields = getSalesforceFieldMap();
const config = body.configurations || {};
@ -77,7 +74,7 @@ export class OrderBuilder {
// Note: Removed fields that can be derived from OrderItems:
// - internetPlanTier: derive from service product metadata
// - installationType: derive from install product name
// - installationType: derive from install product name
// - weekendInstall: derive from SKU analysis
// - hikariDenwa: derive from SKU analysis
}
@ -134,10 +131,7 @@ export class OrderBuilder {
}
}
private addVpnFields(
orderFields: Record<string, unknown>,
body: CreateOrderBody
): void {
private addVpnFields(orderFields: Record<string, unknown>, 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)
}

View File

@ -42,11 +42,14 @@ export class OrderItemBuilder {
throw new Error(`PricebookEntry for SKU ${sku} has no UnitPrice set`);
}
this.logger.log({
sku,
pbeId: meta.pbeId,
unitPrice: meta.unitPrice
}, "Creating OrderItem");
this.logger.log(
{
sku,
pbeId: meta.pbeId,
unitPrice: meta.unitPrice,
},
"Creating OrderItem"
);
try {
// Salesforce requires explicit UnitPrice even with PricebookEntryId
@ -122,7 +125,7 @@ export class OrderItemBuilder {
try {
this.logger.debug({ sku, pricebookId }, "Querying PricebookEntry for SKU");
const res = (await this.sf.query(soql)) as {
records?: Array<{
Id?: string;
@ -132,11 +135,14 @@ export class OrderItemBuilder {
}>;
};
this.logger.debug({
sku,
found: !!(res.records?.length),
hasPrice: !!(res.records?.[0] as any)?.UnitPrice
}, "PricebookEntry query result");
this.logger.debug(
{
sku,
found: !!res.records?.length,
hasPrice: !!(res.records?.[0] as any)?.UnitPrice,
},
"PricebookEntry query result"
);
const rec = res.records?.[0];
if (!rec?.Id) return null;

View File

@ -44,11 +44,7 @@ export class OrderOrchestrator {
);
// 2) Build order fields (simplified - no address snapshot for now)
const orderFields = this.orderBuilder.buildOrderFields(
validatedBody,
userMapping,
pricebookId
);
const orderFields = this.orderBuilder.buildOrderFields(validatedBody, userMapping, pricebookId);
this.logger.log({ orderType: orderFields.Type }, "Creating Salesforce Order");
@ -58,10 +54,13 @@ export class OrderOrchestrator {
created = (await this.sf.sobject("Order").create(orderFields)) as { id: string };
this.logger.log({ orderId: created.id }, "Salesforce Order created successfully");
} catch (error) {
this.logger.error({
error: error instanceof Error ? error.message : String(error),
orderType: orderFields.Type
}, "Failed to create Salesforce Order");
this.logger.error(
{
error: error instanceof Error ? error.message : String(error),
orderType: orderFields.Type,
},
"Failed to create Salesforce Order"
);
throw error;
}

View File

@ -76,11 +76,11 @@ export class OrderValidator {
};
this.logger.debug(
{
{
orderType: validatedBody.orderType,
skuCount: validatedBody.skus.length,
hasConfigurations: !!validatedBody.configurations
},
hasConfigurations: !!validatedBody.configurations,
},
"Request format validation passed"
);
return validatedBody;

View File

@ -567,7 +567,7 @@ export class UsersService {
// Get client details from WHMCS
const clientDetails = await this.whmcsService.getClientDetails(mapping.whmcsClientId);
return {
company: clientDetails.companyname || null,
email: clientDetails.email,

View File

@ -3,13 +3,13 @@
import { useState, useEffect } from "react";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { authenticatedApi } from "@/lib/api";
import {
import {
CreditCardIcon,
MapPinIcon,
PencilIcon,
CheckIcon,
XMarkIcon,
ExclamationTriangleIcon
ExclamationTriangleIcon,
} from "@heroicons/react/24/outline";
interface Address {
@ -55,19 +55,21 @@ export default function BillingPage() {
const handleEdit = () => {
setEditing(true);
setEditedAddress(billingInfo?.address || {
street: "",
streetLine2: "",
city: "",
state: "",
postalCode: "",
country: "",
});
setEditedAddress(
billingInfo?.address || {
street: "",
streetLine2: "",
city: "",
state: "",
postalCode: "",
country: "",
}
);
};
const handleSave = async () => {
if (!editedAddress) return;
try {
setSaving(true);
// TODO: Implement when WHMCS update is available
@ -91,7 +93,9 @@ export default function BillingPage() {
<div className="max-w-4xl mx-auto">
<div className="flex items-center space-x-3 mb-6">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="text-xl font-semibold text-gray-900">Loading billing information...</span>
<span className="text-xl font-semibold text-gray-900">
Loading billing information...
</span>
</div>
</div>
</DashboardLayout>
@ -159,12 +163,14 @@ export default function BillingPage() {
<input
type="text"
value={editedAddress?.street || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, street: e.target.value} : null)}
onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, street: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="123 Main Street"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Street Address Line 2
@ -172,7 +178,11 @@ export default function BillingPage() {
<input
type="text"
value={editedAddress?.streetLine2 || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, streetLine2: e.target.value} : null)}
onChange={e =>
setEditedAddress(prev =>
prev ? { ...prev, streetLine2: e.target.value } : null
)
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Apartment, suite, etc. (optional)"
/>
@ -180,18 +190,18 @@ export default function BillingPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
City *
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">City *</label>
<input
type="text"
value={editedAddress?.city || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, city: e.target.value} : null)}
onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, city: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Tokyo"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
State/Prefecture *
@ -199,7 +209,9 @@ export default function BillingPage() {
<input
type="text"
value={editedAddress?.state || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, state: e.target.value} : null)}
onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, state: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Tokyo"
/>
@ -214,19 +226,27 @@ export default function BillingPage() {
<input
type="text"
value={editedAddress?.postalCode || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, postalCode: e.target.value} : null)}
onChange={e =>
setEditedAddress(prev =>
prev ? { ...prev, postalCode: e.target.value } : null
)
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="100-0001"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country *
</label>
<select
value={editedAddress?.country || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, country: e.target.value} : null)}
onChange={e =>
setEditedAddress(prev =>
prev ? { ...prev, country: e.target.value } : null
)
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select Country</option>
@ -264,26 +284,29 @@ export default function BillingPage() {
<div className="bg-gray-50 rounded-lg p-4">
<div className="text-gray-900">
<p className="font-medium">{billingInfo.address.street}</p>
{billingInfo.address.streetLine2 && (
<p>{billingInfo.address.streetLine2}</p>
)}
{billingInfo.address.streetLine2 && <p>{billingInfo.address.streetLine2}</p>}
<p>
{billingInfo.address.city}, {billingInfo.address.state} {billingInfo.address.postalCode}
{billingInfo.address.city}, {billingInfo.address.state}{" "}
{billingInfo.address.postalCode}
</p>
<p>{billingInfo.address.country}</p>
</div>
<div className="mt-3 pt-3 border-t border-gray-200">
<div className="flex items-center space-x-2">
{billingInfo.isComplete ? (
<>
<CheckIcon className="h-4 w-4 text-green-500" />
<span className="text-sm text-green-700 font-medium">Address Complete</span>
<span className="text-sm text-green-700 font-medium">
Address Complete
</span>
</>
) : (
<>
<ExclamationTriangleIcon className="h-4 w-4 text-yellow-500" />
<span className="text-sm text-yellow-700 font-medium">Address Incomplete</span>
<span className="text-sm text-yellow-700 font-medium">
Address Incomplete
</span>
</>
)}
</div>
@ -341,7 +364,7 @@ export default function BillingPage() {
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-800">
<strong>Note:</strong> Contact information is managed through your account settings.
<strong>Note:</strong> Contact information is managed through your account settings.
Address updates will be available soon.
</p>
</div>

View File

@ -82,13 +82,13 @@ function InternetConfigureContent() {
setPlan(selectedPlan);
setAddons(addonsData);
setInstallations(installationsData);
// Restore state from URL parameters
const accessModeParam = searchParams.get("accessMode");
if (accessModeParam) {
setMode(accessModeParam as AccessMode);
}
const installationSkuParam = searchParams.get("installationSku");
if (installationSkuParam) {
const installation = installationsData.find(i => i.sku === installationSkuParam);
@ -96,7 +96,7 @@ function InternetConfigureContent() {
setInstallPlan(installation.type);
}
}
// Restore selected addons from URL parameters
const addonSkuParams = searchParams.getAll("addonSku");
if (addonSkuParams.length > 0) {
@ -413,7 +413,7 @@ function InternetConfigureContent() {
{/* Continue Button */}
<div className="flex justify-end mt-6">
<AnimatedButton
<AnimatedButton
onClick={() => {
if (!mode) {
return;
@ -517,7 +517,7 @@ function InternetConfigureContent() {
{/* Continue Button */}
<div className="flex justify-between mt-6">
<AnimatedButton
<AnimatedButton
onClick={() => transitionToStep(1)}
variant="outline"
className="flex items-center"
@ -525,7 +525,7 @@ function InternetConfigureContent() {
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back to Service Details
</AnimatedButton>
<AnimatedButton
<AnimatedButton
onClick={() => {
if (!installPlan) {
return;
@ -566,7 +566,7 @@ function InternetConfigureContent() {
/>
{/* Navigation Buttons */}
<div className="flex justify-between mt-6">
<AnimatedButton
<AnimatedButton
onClick={() => transitionToStep(2)}
variant="outline"
className="flex items-center"
@ -574,10 +574,7 @@ function InternetConfigureContent() {
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back to Installation
</AnimatedButton>
<AnimatedButton
onClick={() => transitionToStep(4)}
className="flex items-center"
>
<AnimatedButton onClick={() => transitionToStep(4)} className="flex items-center">
Review Order
<ArrowRightIcon className="w-4 h-4 ml-2" />
</AnimatedButton>
@ -617,7 +614,9 @@ function InternetConfigureContent() {
<p className="text-sm text-gray-600">Internet Service</p>
</div>
<div className="text-right">
<p className="font-semibold text-gray-900">¥{plan.monthlyPrice?.toLocaleString()}</p>
<p className="font-semibold text-gray-900">
¥{plan.monthlyPrice?.toLocaleString()}
</p>
<p className="text-xs text-gray-500">per month</p>
</div>
</div>
@ -645,7 +644,12 @@ function InternetConfigureContent() {
<div key={addonSku} className="flex justify-between text-sm">
<span className="text-gray-600">{addon?.name || addonSku}</span>
<span className="text-gray-900">
¥{(addon?.monthlyPrice || addon?.activationPrice || 0).toLocaleString()}
¥
{(
addon?.monthlyPrice ||
addon?.activationPrice ||
0
).toLocaleString()}
<span className="text-xs text-gray-500 ml-1">
/{addon?.monthlyPrice ? "mo" : "once"}
</span>
@ -686,7 +690,9 @@ function InternetConfigureContent() {
{oneTimeTotal > 0 && (
<div className="flex justify-between text-sm">
<span className="text-gray-600">One-time Total</span>
<span className="text-orange-600 font-semibold">¥{oneTimeTotal.toLocaleString()}</span>
<span className="text-orange-600 font-semibold">
¥{oneTimeTotal.toLocaleString()}
</span>
</div>
)}
</div>
@ -694,9 +700,7 @@ function InternetConfigureContent() {
{/* Receipt Footer */}
<div className="text-center mt-6 pt-4 border-t border-gray-200">
<p className="text-xs text-gray-500">
High-speed internet service
</p>
<p className="text-xs text-gray-500">High-speed internet service</p>
</div>
</div>

View File

@ -88,39 +88,42 @@ function SimConfigureContent() {
setPlan(foundPlan);
setActivationFees(fees);
setAddons(addonsData);
// Restore state from URL parameters
const simTypeParam = searchParams.get("simType");
if (simTypeParam && (simTypeParam === "eSIM" || simTypeParam === "Physical SIM")) {
setSimType(simTypeParam);
}
const eidParam = searchParams.get("eid");
if (eidParam) {
setEid(eidParam);
}
const activationTypeParam = searchParams.get("activationType");
if (activationTypeParam && (activationTypeParam === "Immediate" || activationTypeParam === "Scheduled")) {
if (
activationTypeParam &&
(activationTypeParam === "Immediate" || activationTypeParam === "Scheduled")
) {
setActivationType(activationTypeParam);
}
const scheduledAtParam = searchParams.get("scheduledAt");
if (scheduledAtParam) {
setScheduledActivationDate(scheduledAtParam);
}
// Restore selected addons from URL parameters
const addonSkuParams = searchParams.getAll("addonSku");
if (addonSkuParams.length > 0) {
setSelectedAddons(addonSkuParams);
}
// Restore MNP data from URL parameters
const isMnpParam = searchParams.get("isMnp");
if (isMnpParam === "true") {
setWantsMnp(true);
// Restore all MNP fields
const reservationNumber = searchParams.get("reservationNumber");
const expiryDate = searchParams.get("expiryDate");
@ -132,7 +135,7 @@ function SimConfigureContent() {
const portingFirstNameKatakana = searchParams.get("portingFirstNameKatakana");
const portingGender = searchParams.get("portingGender");
const portingDateOfBirth = searchParams.get("portingDateOfBirth");
setMnpData({
reservationNumber: reservationNumber || "",
expiryDate: expiryDate || "",
@ -142,7 +145,12 @@ function SimConfigureContent() {
portingFirstName: portingFirstName || "",
portingLastNameKatakana: portingLastNameKatakana || "",
portingFirstNameKatakana: portingFirstNameKatakana || "",
portingGender: (portingGender === "Male" || portingGender === "Female" || portingGender === "Corporate/Other") ? portingGender : "",
portingGender:
portingGender === "Male" ||
portingGender === "Female" ||
portingGender === "Corporate/Other"
? portingGender
: "",
portingDateOfBirth: portingDateOfBirth || "",
});
}
@ -236,7 +244,7 @@ function SimConfigureContent() {
// Smooth step transition function - preserves user data
const transitionToStep = (nextStep: number) => {
setIsTransitioning(true);
setTimeout(() => {
setCurrentStep(nextStep);
setTimeout(() => {
@ -434,14 +442,14 @@ function SimConfigureContent() {
isTransitioning ? "opacity-0 translate-y-4" : "opacity-100 translate-y-0"
}`}
>
<StepHeader
stepNumber={1}
title="SIM Type Selection"
description="Choose the type of SIM card for your device"
/>
<StepHeader
stepNumber={1}
title="SIM Type Selection"
description="Choose the type of SIM card for your device"
/>
<SimTypeSelector
simType={simType}
onSimTypeChange={(type) => setSimType(type)}
onSimTypeChange={type => setSimType(type)}
eid={eid}
onEidChange={setEid}
errors={errors}
@ -449,7 +457,7 @@ function SimConfigureContent() {
{/* Continue Button */}
<div className="flex justify-end mt-6">
<AnimatedButton
<AnimatedButton
onClick={() => {
// Validate step 1 before proceeding
if (!simType) {
@ -499,7 +507,7 @@ function SimConfigureContent() {
{/* Navigation Buttons */}
<div className="flex justify-between mt-6">
<AnimatedButton
<AnimatedButton
onClick={() => transitionToStep(1)}
variant="outline"
className="flex items-center"
@ -507,7 +515,7 @@ function SimConfigureContent() {
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back to SIM Type
</AnimatedButton>
<AnimatedButton
<AnimatedButton
onClick={() => {
// Validate step 2 before proceeding
if (!activationType) {
@ -521,7 +529,9 @@ function SimConfigureContent() {
setErrors({}); // Clear errors
transitionToStep(3);
}}
disabled={!activationType || (activationType === "Scheduled" && !scheduledActivationDate)}
disabled={
!activationType || (activationType === "Scheduled" && !scheduledActivationDate)
}
className="flex items-center"
>
Continue to Add-ons
@ -543,7 +553,11 @@ function SimConfigureContent() {
<StepHeader
stepNumber={3}
title={plan.planType === "DataOnly" ? "Add-ons" : "Voice Add-ons"}
description={plan.planType === "DataOnly" ? "No add-ons available for data-only plans" : "Enhance your voice services with these optional features"}
description={
plan.planType === "DataOnly"
? "No add-ons available for data-only plans"
: "Enhance your voice services with these optional features"
}
/>
</div>
@ -567,8 +581,8 @@ function SimConfigureContent() {
) : (
<div className="text-center py-8">
<p className="text-gray-600">
{plan.planType === "DataOnly"
? "No add-ons are available for data-only plans."
{plan.planType === "DataOnly"
? "No add-ons are available for data-only plans."
: "No add-ons are available for this plan."}
</p>
</div>
@ -576,7 +590,7 @@ function SimConfigureContent() {
{/* Navigation Buttons */}
<div className="flex justify-between mt-6">
<AnimatedButton
<AnimatedButton
onClick={() => transitionToStep(2)}
variant="outline"
className="flex items-center"
@ -584,10 +598,7 @@ function SimConfigureContent() {
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back to Activation
</AnimatedButton>
<AnimatedButton
onClick={() => transitionToStep(4)}
className="flex items-center"
>
<AnimatedButton onClick={() => transitionToStep(4)} className="flex items-center">
Continue to Number Porting
<ArrowRightIcon className="w-4 h-4 ml-2" />
</AnimatedButton>
@ -621,7 +632,7 @@ function SimConfigureContent() {
{/* Navigation Buttons */}
<div className="flex justify-between mt-6">
<AnimatedButton
<AnimatedButton
onClick={() => transitionToStep(3)}
variant="outline"
className="flex items-center"
@ -629,7 +640,7 @@ function SimConfigureContent() {
<ArrowLeftIcon className="w-4 h-4 mr-2" />
Back to Add-ons
</AnimatedButton>
<AnimatedButton
<AnimatedButton
onClick={() => {
// Validate MNP form if MNP is selected
if (wantsMnp && !validateForm()) {
@ -679,7 +690,9 @@ function SimConfigureContent() {
<p className="text-sm text-gray-600">{plan.dataSize}</p>
</div>
<div className="text-right">
<p className="font-semibold text-gray-900">¥{plan.monthlyPrice?.toLocaleString()}</p>
<p className="font-semibold text-gray-900">
¥{plan.monthlyPrice?.toLocaleString()}
</p>
<p className="text-xs text-gray-500">per month</p>
</div>
</div>
@ -696,14 +709,16 @@ function SimConfigureContent() {
{simType === "eSIM" && eid && (
<div className="flex justify-between">
<span className="text-gray-600">EID:</span>
<span className="text-gray-900 font-mono text-xs">{eid.substring(0, 12)}...</span>
<span className="text-gray-900 font-mono text-xs">
{eid.substring(0, 12)}...
</span>
</div>
)}
<div className="flex justify-between">
<span className="text-gray-600">Activation:</span>
<span className="text-gray-900">
{activationType === "Scheduled" && scheduledActivationDate
? `${new Date(scheduledActivationDate).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`
{activationType === "Scheduled" && scheduledActivationDate
? `${new Date(scheduledActivationDate).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`
: activationType || "Not selected"}
</span>
</div>
@ -764,7 +779,9 @@ function SimConfigureContent() {
{oneTimeTotal > 0 && (
<div className="flex justify-between text-sm">
<span className="text-gray-600">One-time Total</span>
<span className="text-orange-600 font-semibold">¥{oneTimeTotal.toLocaleString()}</span>
<span className="text-orange-600 font-semibold">
¥{oneTimeTotal.toLocaleString()}
</span>
</div>
)}
</div>

View File

@ -90,7 +90,9 @@ function CheckoutContent() {
const plan = plans.find(p => p.sku === selections.plan);
if (!plan) {
throw new Error(`Internet plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`);
throw new Error(
`Internet plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`
);
}
// Handle addon SKUs like SIM flow
@ -116,21 +118,23 @@ function CheckoutContent() {
const plan = plans.find(p => p.sku === selections.plan); // Look up by SKU instead of ID
if (!plan) {
throw new Error(`SIM plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`);
throw new Error(
`SIM plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`
);
}
// Handle multiple addons from URL parameters
const addonSkus: string[] = [];
if (selections.addonSku) {
// Single addon (legacy support)
addonSkus.push(selections.addonSku);
// Handle multiple addons from URL parameters
const addonSkus: string[] = [];
if (selections.addonSku) {
// Single addon (legacy support)
addonSkus.push(selections.addonSku);
}
// Check for multiple addonSku parameters
const urlParams = new URLSearchParams(window.location.search);
urlParams.getAll("addonSku").forEach(sku => {
if (sku && !addonSkus.includes(sku)) {
addonSkus.push(sku);
}
// Check for multiple addonSku parameters
const urlParams = new URLSearchParams(window.location.search);
urlParams.getAll("addonSku").forEach(sku => {
if (sku && !addonSkus.includes(sku)) {
addonSkus.push(sku);
}
});
});
orderItems = buildSimOrderItems(plan, activationFees, addons, {
addonSkus: addonSkus.length > 0 ? addonSkus : undefined,
@ -210,7 +214,7 @@ function CheckoutContent() {
...(confirmedAddress && { address: confirmedAddress }),
};
const response = await authenticatedApi.post<{sfOrderId: string}>("/orders", orderData);
const response = await authenticatedApi.post<{ sfOrderId: string }>("/orders", orderData);
router.push(`/orders/${response.sfOrderId}?status=success`);
} catch (error) {
console.error("Order submission failed:", error);
@ -288,8 +292,8 @@ function CheckoutContent() {
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Submit Your Order for Review</h2>
<p className="text-gray-600 mb-4">
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.
</p>
<div className="bg-white rounded-lg p-4 border border-blue-200">
<h3 className="font-semibold text-gray-900 mb-2">What happens next?</h3>
@ -300,7 +304,7 @@ function CheckoutContent() {
<p> You'll receive confirmation once everything is ready</p>
</div>
</div>
{/* Quick Totals Summary */}
<div className="mt-4 bg-white rounded-lg p-4 border border-gray-200">
<div className="flex justify-between items-center">
@ -325,14 +329,18 @@ function CheckoutContent() {
<div className="flex items-start gap-3">
<div className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</div>
<div>
<p className="text-green-800 text-sm font-medium">Payment method verified</p>
<p className="text-green-700 text-sm mt-1">
After order approval, payment will be automatically processed using your existing payment method on file.
No additional payment steps required.
After order approval, payment will be automatically processed using your existing
payment method on file. No additional payment steps required.
</p>
</div>
</div>
@ -347,10 +355,11 @@ function CheckoutContent() {
const urlParams = new URLSearchParams(params.toString());
const reviewStep = orderType === "Internet" ? "4" : "5";
urlParams.set("step", reviewStep);
const configureUrl = orderType === "Internet"
? `/catalog/internet/configure?${urlParams.toString()}`
: `/catalog/sim/configure?${urlParams.toString()}`;
const configureUrl =
orderType === "Internet"
? `/catalog/internet/configure?${urlParams.toString()}`
: `/catalog/sim/configure?${urlParams.toString()}`;
router.push(configureUrl);
}}
className="flex-1 px-6 py-4 border border-gray-300 rounded-lg text-center hover:bg-gray-50 transition-colors font-medium"
@ -365,9 +374,25 @@ function CheckoutContent() {
>
{submitting ? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Submitting Order...
</span>

View File

@ -43,7 +43,7 @@ export default function OrderStatusPage() {
const searchParams = useSearchParams();
const [data, setData] = useState<OrderSummary | null>(null);
const [error, setError] = useState<string | null>(null);
const isNewOrder = searchParams.get('status') === 'success';
const isNewOrder = searchParams.get("status") === "success";
useEffect(() => {
let mounted = true;
@ -83,7 +83,8 @@ export default function OrderStatusPage() {
Order Submitted Successfully!
</h3>
<p className="text-green-800 mb-3">
Your order has been created and submitted for processing. We'll notify you as soon as it's approved and ready for activation.
Your order has been created and submitted for processing. We'll notify you as soon
as it's approved and ready for activation.
</p>
<div className="text-sm text-green-700">
<p className="mb-1">

View File

@ -30,7 +30,7 @@ export default function OrdersPage() {
const [orders, setOrders] = useState<OrderSummary[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const showSuccess = searchParams.get('status') === 'success';
const showSuccess = searchParams.get("status") === "success";
useEffect(() => {
const fetchOrders = async () => {
@ -75,7 +75,8 @@ export default function OrdersPage() {
Order Submitted Successfully!
</h3>
<p className="text-green-800">
Your order has been created and is now being processed. You can track its progress below.
Your order has been created and is now being processed. You can track its progress
below.
</p>
</div>
</div>
@ -107,7 +108,7 @@ export default function OrdersPage() {
</div>
) : (
<div className="space-y-4">
{orders.map((order) => (
{orders.map(order => (
<div
key={order.id}
className="bg-white border rounded-xl p-6 hover:shadow-md transition-shadow cursor-pointer"
@ -122,19 +123,23 @@ export default function OrdersPage() {
{order.orderType} {new Date(order.createdDate).toLocaleDateString()}
</p>
</div>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(order.status)}`}>
<span
className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(order.status)}`}
>
{order.status}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="font-medium text-gray-700">Items:</span>
<p className="text-gray-600">
{order.itemSummary ||
(order.itemsSummary && order.itemsSummary.length > 0
? order.itemsSummary.map(item => `${item.name} (${item.quantity})`).join(', ')
: 'No items')}
{order.itemSummary ||
(order.itemsSummary && order.itemsSummary.length > 0
? order.itemsSummary
.map(item => `${item.name} (${item.quantity})`)
.join(", ")
: "No items")}
</p>
</div>
<div>
@ -144,7 +149,9 @@ export default function OrdersPage() {
{order.totalAmount && (
<div>
<span className="font-medium text-gray-700">Total:</span>
<p className="text-gray-900 font-semibold">¥{order.totalAmount.toLocaleString()}</p>
<p className="text-gray-900 font-semibold">
¥{order.totalAmount.toLocaleString()}
</p>
</div>
)}
</div>

View File

@ -27,7 +27,7 @@ export function ActivationForm({
name="activationType"
value="Immediate"
checked={activationType === "Immediate"}
onChange={(e) => onActivationTypeChange(e.target.value as "Immediate")}
onChange={e => onActivationTypeChange(e.target.value as "Immediate")}
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/>
<div>
@ -50,7 +50,7 @@ export function ActivationForm({
name="activationType"
value="Scheduled"
checked={activationType === "Scheduled"}
onChange={(e) => onActivationTypeChange(e.target.value as "Scheduled")}
onChange={e => onActivationTypeChange(e.target.value as "Scheduled")}
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/>
<div className="flex-1">
@ -71,19 +71,17 @@ export function ActivationForm({
type="date"
id="scheduledActivationDate"
value={scheduledActivationDate}
onChange={(e) => onScheduledActivationDateChange(e.target.value)}
onChange={e => onScheduledActivationDateChange(e.target.value)}
min={new Date().toISOString().split("T")[0]} // Today's date
max={
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split("T")[0]
} // 30 days from now
max={new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split("T")[0]} // 30 days from now
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
{errors.scheduledActivationDate && (
<p className="text-red-600 text-sm mt-1">{errors.scheduledActivationDate}</p>
)}
<p className="text-xs text-blue-700 mt-1">
Note: Scheduled activation is subject to business day processing.
Weekend/holiday requests may be processed on the next business day.
Note: Scheduled activation is subject to business day processing. Weekend/holiday
requests may be processed on the next business day.
</p>
</div>
)}

View File

@ -21,7 +21,13 @@ interface MnpFormProps {
errors: Record<string, string>;
}
export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange, errors }: MnpFormProps) {
export function MnpForm({
wantsMnp,
onWantsMnpChange,
mnpData,
onMnpDataChange,
errors,
}: MnpFormProps) {
const handleInputChange = (field: keyof MnpData, value: string) => {
onMnpDataChange({
...mnpData,
@ -36,7 +42,7 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
<input
type="checkbox"
checked={wantsMnp}
onChange={(e) => onWantsMnpChange(e.target.checked)}
onChange={e => onWantsMnpChange(e.target.checked)}
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
/>
<div>
@ -55,21 +61,24 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
<div className="mt-6 p-6 bg-blue-50 rounded-lg border border-blue-200">
<h4 className="font-medium text-blue-900 mb-4">Number Porting Information</h4>
<p className="text-sm text-blue-800 mb-4">
Please provide the following information from your current mobile carrier to complete the
number porting process.
Please provide the following information from your current mobile carrier to complete
the number porting process.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* MNP Reservation Number */}
<div>
<label htmlFor="reservationNumber" className="block text-sm font-medium text-gray-700 mb-1">
<label
htmlFor="reservationNumber"
className="block text-sm font-medium text-gray-700 mb-1"
>
MNP Reservation Number *
</label>
<input
type="text"
id="reservationNumber"
value={mnpData.reservationNumber}
onChange={(e) => handleInputChange("reservationNumber", e.target.value)}
onChange={e => handleInputChange("reservationNumber", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="10-digit reservation number"
/>
@ -87,7 +96,7 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
type="date"
id="expiryDate"
value={mnpData.expiryDate}
onChange={(e) => handleInputChange("expiryDate", e.target.value)}
onChange={e => handleInputChange("expiryDate", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
{errors.expiryDate && (
@ -104,7 +113,7 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
type="tel"
id="phoneNumber"
value={mnpData.phoneNumber}
onChange={(e) => handleInputChange("phoneNumber", e.target.value)}
onChange={e => handleInputChange("phoneNumber", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="090-1234-5678"
/>
@ -115,14 +124,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* MVNO Account Number */}
<div>
<label htmlFor="mvnoAccountNumber" className="block text-sm font-medium text-gray-700 mb-1">
<label
htmlFor="mvnoAccountNumber"
className="block text-sm font-medium text-gray-700 mb-1"
>
Account Number
</label>
<input
type="text"
id="mvnoAccountNumber"
value={mnpData.mvnoAccountNumber}
onChange={(e) => handleInputChange("mvnoAccountNumber", e.target.value)}
onChange={e => handleInputChange("mvnoAccountNumber", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Your current carrier account number"
/>
@ -130,14 +142,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* Last Name */}
<div>
<label htmlFor="portingLastName" className="block text-sm font-medium text-gray-700 mb-1">
<label
htmlFor="portingLastName"
className="block text-sm font-medium text-gray-700 mb-1"
>
Last Name *
</label>
<input
type="text"
id="portingLastName"
value={mnpData.portingLastName}
onChange={(e) => handleInputChange("portingLastName", e.target.value)}
onChange={e => handleInputChange("portingLastName", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Tanaka"
/>
@ -148,14 +163,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* First Name */}
<div>
<label htmlFor="portingFirstName" className="block text-sm font-medium text-gray-700 mb-1">
<label
htmlFor="portingFirstName"
className="block text-sm font-medium text-gray-700 mb-1"
>
First Name *
</label>
<input
type="text"
id="portingFirstName"
value={mnpData.portingFirstName}
onChange={(e) => handleInputChange("portingFirstName", e.target.value)}
onChange={e => handleInputChange("portingFirstName", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Taro"
/>
@ -166,14 +184,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* Last Name Katakana */}
<div>
<label htmlFor="portingLastNameKatakana" className="block text-sm font-medium text-gray-700 mb-1">
<label
htmlFor="portingLastNameKatakana"
className="block text-sm font-medium text-gray-700 mb-1"
>
Last Name (Katakana) *
</label>
<input
type="text"
id="portingLastNameKatakana"
value={mnpData.portingLastNameKatakana}
onChange={(e) => handleInputChange("portingLastNameKatakana", e.target.value)}
onChange={e => handleInputChange("portingLastNameKatakana", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="タナカ"
/>
@ -184,14 +205,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* First Name Katakana */}
<div>
<label htmlFor="portingFirstNameKatakana" className="block text-sm font-medium text-gray-700 mb-1">
<label
htmlFor="portingFirstNameKatakana"
className="block text-sm font-medium text-gray-700 mb-1"
>
First Name (Katakana) *
</label>
<input
type="text"
id="portingFirstNameKatakana"
value={mnpData.portingFirstNameKatakana}
onChange={(e) => handleInputChange("portingFirstNameKatakana", e.target.value)}
onChange={e => handleInputChange("portingFirstNameKatakana", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="タロウ"
/>
@ -202,13 +226,18 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* Gender */}
<div>
<label htmlFor="portingGender" className="block text-sm font-medium text-gray-700 mb-1">
<label
htmlFor="portingGender"
className="block text-sm font-medium text-gray-700 mb-1"
>
Gender *
</label>
<select
id="portingGender"
value={mnpData.portingGender}
onChange={(e) => handleInputChange("portingGender", e.target.value as MnpData["portingGender"])}
onChange={e =>
handleInputChange("portingGender", e.target.value as MnpData["portingGender"])
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="">Select gender</option>
@ -223,14 +252,17 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
{/* Date of Birth */}
<div>
<label htmlFor="portingDateOfBirth" className="block text-sm font-medium text-gray-700 mb-1">
<label
htmlFor="portingDateOfBirth"
className="block text-sm font-medium text-gray-700 mb-1"
>
Date of Birth *
</label>
<input
type="date"
id="portingDateOfBirth"
value={mnpData.portingDateOfBirth}
onChange={(e) => handleInputChange("portingDateOfBirth", e.target.value)}
onChange={e => handleInputChange("portingDateOfBirth", e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
{errors.portingDateOfBirth && (
@ -241,8 +273,8 @@ export function MnpForm({ wantsMnp, onWantsMnpChange, mnpData, onMnpDataChange,
<div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-sm text-yellow-800">
<strong>Important:</strong> Please ensure all information matches exactly with your current
carrier records. Incorrect information may delay the porting process.
<strong>Important:</strong> Please ensure all information matches exactly with your
current carrier records. Incorrect information may delay the porting process.
</p>
</div>
</div>

View File

@ -30,14 +30,16 @@ export function ProgressSteps({ steps, currentStep, className = "" }: ProgressSt
step.completed
? "bg-green-500 border-green-500 text-white shadow-lg scale-110"
: currentStep === step.number
? "border-blue-500 text-blue-500 bg-blue-50 scale-105 shadow-md"
: "border-gray-300 text-gray-400 scale-100"
? "border-blue-500 text-blue-500 bg-blue-50 scale-105 shadow-md"
: "border-gray-300 text-gray-400 scale-100"
}`}
>
{step.completed ? (
<CheckCircleIcon className="w-5 h-5 md:w-7 md:h-7 transition-all duration-300" />
) : (
<span className="font-bold text-sm md:text-base transition-all duration-300">{step.number}</span>
<span className="font-bold text-sm md:text-base transition-all duration-300">
{step.number}
</span>
)}
</div>
<span
@ -45,8 +47,8 @@ export function ProgressSteps({ steps, currentStep, className = "" }: ProgressSt
step.completed
? "text-green-600"
: currentStep === step.number
? "text-blue-600"
: "text-gray-400"
? "text-blue-600"
: "text-gray-400"
}`}
>
{step.title}

View File

@ -30,7 +30,7 @@ export function SimTypeSelector({
name="simType"
value="Physical SIM"
checked={simType === "Physical SIM"}
onChange={(e) => onSimTypeChange(e.target.value as "Physical SIM")}
onChange={e => onSimTypeChange(e.target.value as "Physical SIM")}
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/>
<DevicePhoneMobileIcon className="w-6 h-6 text-blue-600 mt-0.5 flex-shrink-0" />
@ -54,7 +54,7 @@ export function SimTypeSelector({
name="simType"
value="eSIM"
checked={simType === "eSIM"}
onChange={(e) => onSimTypeChange(e.target.value as "eSIM")}
onChange={e => onSimTypeChange(e.target.value as "eSIM")}
className="mt-1 h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500"
/>
<CpuChipIcon className="w-6 h-6 text-blue-600 mt-0.5 flex-shrink-0" />
@ -79,15 +79,15 @@ export function SimTypeSelector({
type="text"
id="eid"
value={eid}
onChange={(e) => onEidChange(e.target.value)}
onChange={e => onEidChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="32-digit EID number"
maxLength={32}
/>
{errors.eid && <p className="text-red-600 text-sm mt-1">{errors.eid}</p>}
<p className="text-xs text-blue-700 mt-1">
Find your EID in: Settings General About EID (iOS) or Settings About Phone
IMEI (Android)
Find your EID in: Settings General About EID (iOS) or Settings About Phone
IMEI (Android)
</p>
</div>
</div>

View File

@ -2,12 +2,12 @@
import { useState, useEffect } from "react";
import { authenticatedApi } from "@/lib/api";
import {
MapPinIcon,
PencilIcon,
CheckIcon,
import {
MapPinIcon,
PencilIcon,
CheckIcon,
XMarkIcon,
ExclamationTriangleIcon
ExclamationTriangleIcon,
} from "@heroicons/react/24/outline";
interface Address {
@ -32,7 +32,10 @@ interface AddressConfirmationProps {
onAddressIncomplete: () => void;
}
export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }: AddressConfirmationProps) {
export function AddressConfirmation({
onAddressConfirmed,
onAddressIncomplete,
}: AddressConfirmationProps) {
const [billingInfo, setBillingInfo] = useState<BillingInfo | null>(null);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false);
@ -48,7 +51,7 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
setLoading(true);
const data = await authenticatedApi.get<BillingInfo>("/users/billing");
setBillingInfo(data);
if (!data.isComplete) {
onAddressIncomplete();
} else {
@ -63,25 +66,27 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
const handleEdit = () => {
setEditing(true);
setEditedAddress(billingInfo?.address || {
street: "",
streetLine2: "",
city: "",
state: "",
postalCode: "",
country: "",
});
setEditedAddress(
billingInfo?.address || {
street: "",
streetLine2: "",
city: "",
state: "",
postalCode: "",
country: "",
}
);
};
const handleSave = async () => {
if (!editedAddress) return;
try {
// For now, just use the edited address for the order
// TODO: Implement WHMCS update when available
onAddressConfirmed(editedAddress);
setEditing(false);
// Update local state to show the new address
if (billingInfo) {
setBillingInfo({
@ -93,7 +98,7 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
editedAddress.state &&
editedAddress.postalCode &&
editedAddress.country
)
),
});
}
} catch (err) {
@ -175,18 +180,18 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
{editing ? (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Street Address *
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">Street Address *</label>
<input
type="text"
value={editedAddress?.street || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, street: e.target.value} : null)}
onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, street: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="123 Main Street"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Street Address Line 2
@ -194,7 +199,9 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
<input
type="text"
value={editedAddress?.streetLine2 || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, streetLine2: e.target.value} : null)}
onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, streetLine2: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Apartment, suite, etc. (optional)"
/>
@ -202,18 +209,18 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
City *
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">City *</label>
<input
type="text"
value={editedAddress?.city || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, city: e.target.value} : null)}
onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, city: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Tokyo"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
State/Prefecture *
@ -221,20 +228,22 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
<input
type="text"
value={editedAddress?.state || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, state: e.target.value} : null)}
onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, state: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Tokyo"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Postal Code *
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">Postal Code *</label>
<input
type="text"
value={editedAddress?.postalCode || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, postalCode: e.target.value} : null)}
onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, postalCode: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="100-0001"
/>
@ -242,12 +251,12 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country *
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">Country *</label>
<select
value={editedAddress?.country || ""}
onChange={(e) => setEditedAddress(prev => prev ? {...prev, country: e.target.value} : null)}
onChange={e =>
setEditedAddress(prev => (prev ? { ...prev, country: e.target.value } : null))
}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select Country</option>
@ -282,11 +291,10 @@ export function AddressConfirmation({ onAddressConfirmed, onAddressIncomplete }:
<div className="bg-gray-50 rounded-lg p-4">
<div className="text-gray-900">
<p className="font-medium">{billingInfo.address.street}</p>
{billingInfo.address.streetLine2 && (
<p>{billingInfo.address.streetLine2}</p>
)}
{billingInfo.address.streetLine2 && <p>{billingInfo.address.streetLine2}</p>}
<p>
{billingInfo.address.city}, {billingInfo.address.state} {billingInfo.address.postalCode}
{billingInfo.address.city}, {billingInfo.address.state}{" "}
{billingInfo.address.postalCode}
</p>
<p>{billingInfo.address.country}</p>
</div>

View File

@ -32,7 +32,7 @@ export async function apiRequest<T>(endpoint: string, options: RequestInit = {})
const finalHeaders: Record<string, string> = {
...defaultHeaders,
...(options.headers as Record<string, string> || {}),
...((options.headers as Record<string, string>) || {}),
};
// Force Content-Type to application/json for POST requests with body
@ -40,8 +40,6 @@ export async function apiRequest<T>(endpoint: string, options: RequestInit = {})
finalHeaders["Content-Type"] = "application/json";
}
const response = await fetch(url, {
...options,
headers: finalHeaders,

View File

@ -51,7 +51,7 @@ issue_cert() {
fi
log "🔐 Issuing certificate for $domain ..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" run --rm -p 80:80 \
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" run --rm -p 80:80 \
-e CERTBOT_DOMAIN="$domain" -e CERTBOT_EMAIL="$email" certbot \
certonly --webroot -w /var/www/certbot -d "$domain" --agree-tos --no-eff-email
log "✅ Certificate issuance process completed"
@ -60,7 +60,7 @@ issue_cert() {
# Build production images
build_images() {
log "🔨 Building production images..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" build --no-cache
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" build --no-cache
log "✅ Images built successfully"
}
@ -73,13 +73,13 @@ deploy() {
# Start database and cache first
log "🗄️ Starting database and cache..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d database cache
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d database cache
# Wait for database
log "⏳ Waiting for database..."
timeout=60
while [ $timeout -gt 0 ]; do
if docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec -T database pg_isready -U portal -d portal_prod 2>/dev/null; then
if docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec -T database pg_isready -U portal -d portal_prod 2>/dev/null; then
log "✅ Database is ready"
break
fi
@ -93,17 +93,17 @@ deploy() {
# Run migrations
log "🔄 Running database migrations..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" run --rm backend pnpm db:migrate || warn "Migration may have failed"
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" run --rm backend pnpm db:migrate || warn "Migration may have failed"
# Start application services (apps only; Plesk handles proxy)
log "🚀 Starting application services..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d frontend backend
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d frontend backend
# Health checks
log "🏥 Performing health checks..."
sleep 15
if docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" ps | grep -q "unhealthy"; then
if docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" ps | grep -q "unhealthy"; then
warn "Some services may not be healthy - check logs"
fi
@ -115,7 +115,7 @@ deploy() {
# Stop production services
stop() {
log "⏹️ Stopping production services..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" down
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" down
log "✅ Services stopped"
}
@ -126,7 +126,7 @@ update() {
build_images
# Zero-downtime update
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d --force-recreate --no-deps backend frontend
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d --force-recreate --no-deps backend frontend
log "✅ Production update completed"
}
@ -134,35 +134,35 @@ update() {
# Show status with health checks
status() {
log "📊 Production Services Status:"
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" ps
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" ps
log "🏥 Health Status:"
# If proxy service exists (non-Plesk mode), check it; otherwise, check frontend directly
if docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" ps proxy >/dev/null 2>&1; then
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec proxy wget --spider -q http://localhost/healthz && echo "✅ Proxy/Frontend healthy" || echo "❌ Proxy/Frontend unhealthy"
if docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" ps proxy >/dev/null 2>&1; then
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec proxy wget --spider -q http://localhost/healthz && echo "✅ Proxy/Frontend healthy" || echo "❌ Proxy/Frontend unhealthy"
else
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec frontend wget --spider -q http://localhost:3000/api/health && echo "✅ Frontend healthy" || echo "❌ Frontend unhealthy"
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec frontend wget --spider -q http://localhost:3000/api/health && echo "✅ Frontend healthy" || echo "❌ Frontend unhealthy"
fi
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec backend wget --spider -q http://localhost:4000/health && echo "✅ Backend healthy" || echo "❌ Backend unhealthy"
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec backend wget --spider -q http://localhost:4000/health && echo "✅ Backend healthy" || echo "❌ Backend unhealthy"
}
# Show logs
logs() {
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" logs -f "${@:2}"
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" logs -f "${@:2}"
}
# Backup database
backup() {
log "💾 Creating database backup..."
backup_file="backup_$(date +%Y%m%d_%H%M%S).sql"
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec -T database pg_dump -U portal portal_prod > "$backup_file"
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" exec -T database pg_dump -U portal portal_prod > "$backup_file"
log "✅ Database backup created: $backup_file"
}
# Clean up
cleanup() {
log "🧹 Cleaning up old containers and images..."
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" down --remove-orphans
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" down --remove-orphans
docker system prune -f
docker image prune -f
log "✅ Cleanup completed"
@ -172,12 +172,12 @@ cleanup() {
case "${1:-help}" in
"deploy") deploy ;;
"start")
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" up -d
log "✅ Production services started"
;;
"stop") stop ;;
"restart")
docker-compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" restart
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT_NAME" restart
log "✅ Services restarted"
;;
"update") update ;;