From 7abd433d957702f6a1a5b2e1dc2eb2dd5f6c95e0 Mon Sep 17 00:00:00 2001
From: barsa
Date: Tue, 3 Feb 2026 18:28:38 +0900
Subject: [PATCH] Refactor conditional rendering and improve code readability
across multiple components
- Simplified conditional rendering in OrderSummary, ProductCard, InstallationOptions, InternetOfferingCard, DeviceCompatibility, SimPlansContent, and other components by removing unnecessary parentheses.
- Enhanced clarity in the use of ternary operators for better maintainability.
- Updated documentation to reflect changes in development setup for skipping OTP verification during login.
- Removed outdated orchestrator refactoring plan document.
- Added new environment variable for skipping OTP verification in development.
- Minor adjustments in domain contracts and mappers for consistency in conditional checks.
---
apps/bff/scripts/check-sim-status.mjs | 35 +-
apps/bff/sim-api-test-log.csv | 89 ---
apps/bff/src/core/config/auth-dev.config.ts | 4 +
apps/bff/src/core/http/exception.filter.ts | 4 +-
.../services/freebit-client.service.ts | 8 +-
.../auth/presentation/http/auth.controller.ts | 18 +-
.../orders/services/checkout.service.ts | 2 +-
.../services/order-validator.service.ts | 4 +-
.../validators/internet-order.validator.ts | 4 +-
.../workflow/workflow-case-manager.service.ts | 4 +-
.../services/internet-cancellation.service.ts | 4 +-
.../services/sim-validation.service.ts | 4 +-
.../verification/residence-card.service.ts | 4 +-
apps/portal/next.config.mjs | 7 +-
.../molecules/OtpInput/OtpInput.tsx | 4 +-
.../organisms/AppShell/AppShell.tsx | 4 +-
.../account/components/AddressCard.tsx | 4 +-
.../address/components/AddressStepJapan.tsx | 4 +-
.../address/components/JapanAddressForm.tsx | 4 +-
.../address/components/ProgressIndicator.tsx | 4 +-
.../src/features/auth/stores/auth.store.ts | 22 +-
.../features/billing/views/PaymentMethods.tsx | 4 +-
.../components/CheckoutStatusBanners.tsx | 4 +-
.../IdentityVerificationSection.tsx | 4 +-
.../landing-page/views/PublicLandingView.tsx | 4 +-
.../components/NotificationDropdown.tsx | 4 +-
.../services/components/base/AddressForm.tsx | 4 +-
.../services/components/base/OrderSummary.tsx | 4 +-
.../services/components/base/ProductCard.tsx | 4 +-
.../internet/InstallationOptions.tsx | 4 +-
.../internet/InternetOfferingCard.tsx | 4 +-
.../components/sim/DeviceCompatibility.tsx | 4 +-
.../components/sim/SimPlansContent.tsx | 4 +-
.../services/hooks/useConfigureParams.ts | 4 +-
.../views/InternetEligibilityRequest.tsx | 4 +-
.../services/views/PublicEligibilityCheck.tsx | 4 +-
.../services/views/PublicInternetPlans.tsx | 4 +-
.../components/sim/ReissueSimModal.tsx | 4 +-
.../components/sim/SimActions.tsx | 4 +-
.../components/sim/SimManagementSection.tsx | 8 +-
.../views/CancelSubscription.tsx | 8 +-
.../subscriptions/views/SimChangePlan.tsx | 8 +-
.../subscriptions/views/SimReissue.tsx | 8 +-
.../features/subscriptions/views/SimTopUp.tsx | 4 +-
.../views/SubscriptionDetail.tsx | 4 +-
.../support/views/NewSupportCaseView.tsx | 4 +-
.../support/views/SupportCasesView.tsx | 4 +-
docs/development/auth/development-setup.md | 3 +
.../orchestrator-refactoring-plan.md | 729 ------------------
env/dev.env.sample | 1 +
packages/domain/opportunity/contract.ts | 2 +-
.../subscriptions/providers/whmcs/mapper.ts | 8 +-
52 files changed, 159 insertions(+), 941 deletions(-)
delete mode 100644 docs/refactoring/orchestrator-refactoring-plan.md
diff --git a/apps/bff/scripts/check-sim-status.mjs b/apps/bff/scripts/check-sim-status.mjs
index 756e961c..56c98f15 100644
--- a/apps/bff/scripts/check-sim-status.mjs
+++ b/apps/bff/scripts/check-sim-status.mjs
@@ -4,11 +4,11 @@
* Usage: node scripts/check-sim-status.mjs
*/
-const account = process.argv[2] || '02000002470010';
+const account = process.argv[2] || "02000002470010";
-const FREEBIT_BASE_URL = 'https://i1-q.mvno.net/emptool/api';
-const FREEBIT_OEM_ID = 'PASI';
-const FREEBIT_OEM_KEY = '6Au3o7wrQNR07JxFHPmf0YfFqN9a31t5';
+const FREEBIT_BASE_URL = "https://i1-q.mvno.net/emptool/api";
+const FREEBIT_OEM_ID = "PASI";
+const FREEBIT_OEM_KEY = "6Au3o7wrQNR07JxFHPmf0YfFqN9a31t5";
async function getAuthKey() {
const request = {
@@ -17,8 +17,8 @@ async function getAuthKey() {
};
const response = await fetch(`${FREEBIT_BASE_URL}/authOem/`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ method: "POST",
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `json=${JSON.stringify(request)}`,
});
@@ -27,7 +27,7 @@ async function getAuthKey() {
}
const data = await response.json();
- if (data.resultCode !== 100 && data.resultCode !== '100') {
+ if (data.resultCode !== 100 && data.resultCode !== "100") {
throw new Error(`Auth failed: ${data.status?.message || JSON.stringify(data)}`);
}
@@ -38,8 +38,8 @@ async function getTrafficInfo(authKey, account) {
const request = { authKey, account };
const response = await fetch(`${FREEBIT_BASE_URL}/mvno/getTrafficInfo/`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ method: "POST",
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `json=${JSON.stringify(request)}`,
});
return response.json();
@@ -48,13 +48,13 @@ async function getTrafficInfo(authKey, account) {
async function getAccountDetails(authKey, account) {
const request = {
authKey,
- version: '2',
- requestDatas: [{ kind: 'MVNO', account }],
+ version: "2",
+ requestDatas: [{ kind: "MVNO", account }],
};
const response = await fetch(`${FREEBIT_BASE_URL}/master/getAcnt/`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ method: "POST",
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `json=${JSON.stringify(request)}`,
});
return response.json();
@@ -65,20 +65,19 @@ async function main() {
try {
const authKey = await getAuthKey();
- console.log('✓ Authenticated with Freebit\n');
+ console.log("✓ Authenticated with Freebit\n");
// Try getTrafficInfo first (simpler)
- console.log('--- Traffic Info (/mvno/getTrafficInfo/) ---');
+ console.log("--- Traffic Info (/mvno/getTrafficInfo/) ---");
const trafficInfo = await getTrafficInfo(authKey, account);
console.log(JSON.stringify(trafficInfo, null, 2));
// Try getAcnt for full details
- console.log('\n--- Account Details (/master/getAcnt/) ---');
+ console.log("\n--- Account Details (/master/getAcnt/) ---");
const details = await getAccountDetails(authKey, account);
console.log(JSON.stringify(details, null, 2));
-
} catch (error) {
- console.error('❌ Error:', error.message);
+ console.error("❌ Error:", error.message);
}
}
diff --git a/apps/bff/sim-api-test-log.csv b/apps/bff/sim-api-test-log.csv
index 7ece45ff..2fa55d66 100644
--- a/apps/bff/sim-api-test-log.csv
+++ b/apps/bff/sim-api-test-log.csv
@@ -1,90 +1 @@
Timestamp,API Endpoint,API Method,Phone Number,SIM Identifier,Request Payload,Response Status,Error,Additional Info
-2026-01-31T02:21:03.485Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T02:21:07.599Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T04:11:11.315Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T04:11:15.556Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T04:11:53.182Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T04:32:18.526Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T04:32:22.394Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T04:32:37.351Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T04:32:41.487Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T04:48:41.057Z,/mvno/changePlan/,POST,02000331144508,02000331144508,"{""account"":""02000331144508"",""planCode"":""PASI_5G"",""runTime"":""20260301""}",Error: 211,API Error: NG,API Error: NG
-2026-01-31T04:49:40.396Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T04:49:44.170Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T04:50:51.053Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T04:50:56.134Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T05:04:11.957Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T05:04:16.274Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T05:11:55.749Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T05:11:59.557Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T05:18:00.675Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T05:18:06.042Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T08:37:08.201Z,/master/addSpec/,POST,02000331144508,02000331144508,"{""account"":""02000331144508"",""quota"":1024000}",Error: 200,API Error: Bad Request,API Error: Bad Request
-2026-01-31T08:45:14.336Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T08:45:18.452Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T08:45:40.760Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T08:45:47.572Z,/mvno/getTrafficInfo/,POST,02000331144508,02000331144508,"{""account"":""02000331144508""}",Error: 210,API Error: NG,API Error: NG
-2026-01-31T08:49:32.767Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T08:49:32.948Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T08:50:04.739Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T08:50:05.899Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T08:55:27.913Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T08:55:28.280Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T08:55:39.246Z,/master/addSpec/,POST,02000215161148,02000215161148,"{""account"":""02000215161148"",""quota"":1024000}",Error: 200,API Error: Bad Request,API Error: Bad Request
-2026-01-31T09:03:45.084Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:03:45.276Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:04:02.612Z,/master/addSpec/,POST,02000215161148,02000215161148,"{""account"":""02000215161148"",""quota"":1000,""kind"":""MVNO""}",Success,,OK
-2026-01-31T09:12:19.280Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:12:19.508Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:12:25.347Z,/mvno/changePlan/,POST,02000215161148,02000215161148,"{""account"":""02000215161148"",""planCode"":""PASI_10G"",""runTime"":""20260301""}",Success,,OK
-2026-01-31T09:13:15.309Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:13:15.522Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:21:56.856Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:21:57.041Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:23:40.211Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:24:26.592Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:24:26.830Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:24:49.713Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:24:49.910Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:25:40.613Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:25:53.426Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:26:05.126Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:26:18.482Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-01-31T09:26:57.215Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T01:48:36.804Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T01:48:37.013Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T01:49:41.283Z,/mvno/changePlan/,POST,02000215161148,02000215161148,"{""account"":""02000215161148"",""planCode"":""PASI_10G"",""runTime"":""20260301""}",Success,,OK
-2026-02-02T01:50:58.940Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T01:50:59.121Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T01:51:07.911Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T02:49:01.626Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T02:49:01.781Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T02:49:04.551Z,/mvno/contractline/change/,POST,02000215161148,02000215161148,"{""account"":""02000215161148"",""contractLine"":""5G"",""eid"":""89033023426200000000006103081142""}",Error: 204,API Error: Bad Request,API Error: Bad Request
-2026-02-02T02:49:04.804Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T02:52:39.440Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T02:52:39.696Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T02:52:43.402Z,/mvno/contractline/change/,POST,02000215161148,02000215161148,"{""account"":""02000215161148"",""contractLine"":""5G"",""eid"":""89033023426200000000006103081142""}",Error: 204,API Error: Bad Request,API Error: Bad Request
-2026-02-02T02:52:43.557Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T02:52:50.419Z,/mvno/contractline/change/,POST,02000215161148,02000215161148,"{""account"":""02000215161148"",""contractLine"":""5G"",""eid"":""89033023426200000000006103081142""}",Error: 204,API Error: Bad Request,API Error: Bad Request
-2026-02-02T02:52:50.595Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T02:52:58.616Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T02:52:58.762Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T02:53:01.434Z,/mvno/contractline/change/,POST,02000215161148,02000215161148,"{""account"":""02000215161148"",""contractLine"":""5G"",""eid"":""89033023426200000000006103081142""}",Error: 204,API Error: Bad Request,API Error: Bad Request
-2026-02-02T02:53:01.580Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T03:00:20.821Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T03:00:21.068Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T03:00:25.799Z,/mvno/contractline/change/,POST,02000215161148,02000215161148,"{""account"":""02000215161148"",""contractLine"":""5G"",""eid"":""89033023426200000000006103081142""}",Error: 204,API Error: Bad Request,API Error: Bad Request
-2026-02-02T03:00:26.012Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T04:14:20.988Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T04:14:21.197Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T04:14:23.599Z,/mvno/contractline/change/,POST,02000215161148,02000215161148,"{""account"":""02000215161148"",""contractLine"":""5G"",""eid"":""89033023426200000000006103081142""}",Error: 204,API Error: Bad Request,API Error: Bad Request
-2026-02-02T04:14:23.805Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T04:17:24.519Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T04:17:24.698Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T04:27:46.948Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T04:27:47.130Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-02T04:27:59.150Z,/mvno/contractline/change/,POST,02000215161148,02000215161148,"{""account"":""02000215161148"",""contractLine"":""5G"",""eid"":""89033023426200000000006103081142""}",Success,,OK
-2026-02-03T02:22:24.012Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-03T02:22:24.263Z,/mvno/getTrafficInfo/,POST,02000215161148,02000215161148,"{""account"":""02000215161148""}",Success,,OK
-2026-02-03T02:44:57.675Z,/mvno/semiblack/addAcnt/,POST,02000002470010,02000002470010,"{""createType"":""new"",""account"":""02000002470010"",""productNumber"":""PT0220024700100"",""planCode"":""PASI_5G"",""shipDate"":""20260203"",""mnp"":{""method"":""10""},""globalIp"":""20"",""aladinOperated"":""20""}",Success,,OK
-2026-02-03T02:55:57.379Z,/mvno/semiblack/addAcnt/,POST,07000240050,07000240050,"{""createType"":""new"",""account"":""07000240050"",""productNumber"":""PT0270002400500"",""planCode"":""PASI_10G"",""shipDate"":""20260203"",""mnp"":{""method"":""10""},""globalIp"":""20"",""aladinOperated"":""20""}",Success,,OK
diff --git a/apps/bff/src/core/config/auth-dev.config.ts b/apps/bff/src/core/config/auth-dev.config.ts
index 99d94ce8..953ae9dc 100644
--- a/apps/bff/src/core/config/auth-dev.config.ts
+++ b/apps/bff/src/core/config/auth-dev.config.ts
@@ -9,6 +9,7 @@ export interface DevAuthConfig {
disableAccountLocking: boolean;
enableDebugLogs: boolean;
simplifiedErrorMessages: boolean;
+ skipOtp: boolean;
}
export const createDevAuthConfig = (): DevAuthConfig => {
@@ -29,6 +30,9 @@ export const createDevAuthConfig = (): DevAuthConfig => {
// Show detailed error messages in development
simplifiedErrorMessages: isDevelopment,
+
+ // Skip OTP verification in development (login directly after credentials)
+ skipOtp: isDevelopment && process.env["SKIP_OTP"] === "true",
};
};
diff --git a/apps/bff/src/core/http/exception.filter.ts b/apps/bff/src/core/http/exception.filter.ts
index 1e0baabd..f6d59ac3 100644
--- a/apps/bff/src/core/http/exception.filter.ts
+++ b/apps/bff/src/core/http/exception.filter.ts
@@ -232,9 +232,9 @@ export class UnifiedExceptionFilter implements ExceptionFilter {
userAgent:
typeof userAgentHeader === "string"
? userAgentHeader
- : (Array.isArray(userAgentHeader)
+ : Array.isArray(userAgentHeader)
? userAgentHeader[0]
- : undefined),
+ : undefined,
ip: request.ip,
};
}
diff --git a/apps/bff/src/integrations/freebit/services/freebit-client.service.ts b/apps/bff/src/integrations/freebit/services/freebit-client.service.ts
index badf35b4..053da70c 100644
--- a/apps/bff/src/integrations/freebit/services/freebit-client.service.ts
+++ b/apps/bff/src/integrations/freebit/services/freebit-client.service.ts
@@ -354,17 +354,17 @@ export class FreebitClientService {
const timestamp = this.testTracker.getCurrentTimestamp();
const resultCode = response?.resultCode
? String(response.resultCode)
- : (error instanceof FreebitError
+ : error instanceof FreebitError
? String(error.resultCode || "ERROR")
- : "ERROR");
+ : "ERROR";
const statusMessage =
response?.status?.message ||
(error instanceof FreebitError
? error.message
- : (error
+ : error
? extractErrorMessage(error)
- : "Success"));
+ : "Success");
await this.testTracker.logApiCall({
timestamp,
diff --git a/apps/bff/src/modules/auth/presentation/http/auth.controller.ts b/apps/bff/src/modules/auth/presentation/http/auth.controller.ts
index 0355b61a..b4d3f7bc 100644
--- a/apps/bff/src/modules/auth/presentation/http/auth.controller.ts
+++ b/apps/bff/src/modules/auth/presentation/http/auth.controller.ts
@@ -36,6 +36,7 @@ import {
REFRESH_COOKIE_PATH,
TOKEN_TYPE,
} from "./utils/auth-cookie.util.js";
+import { devAuthConfig } from "@bff/core/config/auth-dev.config.js";
// Import Zod schemas from domain
import {
@@ -130,6 +131,21 @@ export class AuthController {
@Req() req: RequestWithUser & RequestWithRateLimit,
@Res({ passthrough: true }) res: Response
) {
+ this.applyAuthRateLimitHeaders(req, res);
+
+ // In dev mode with SKIP_OTP=true, skip OTP and complete login directly
+ if (devAuthConfig.skipOtp) {
+ const loginResult = await this.authOrchestrator.completeLogin(
+ { id: req.user.id, email: req.user.email, role: req.user.role ?? "USER" },
+ req
+ );
+ setAuthCookies(res, loginResult.tokens);
+ return {
+ user: loginResult.user,
+ session: buildSessionInfo(loginResult.tokens),
+ };
+ }
+
// Credentials validated by LocalAuthGuard - now initiate OTP
const fingerprint = getRequestFingerprint(req);
const otpResult = await this.loginOtpWorkflow.initiateOtp(
@@ -141,8 +157,6 @@ export class AuthController {
fingerprint
);
- this.applyAuthRateLimitHeaders(req, res);
-
// Return OTP required response - no tokens issued yet
return {
requiresOtp: true,
diff --git a/apps/bff/src/modules/orders/services/checkout.service.ts b/apps/bff/src/modules/orders/services/checkout.service.ts
index d3c12431..ea33c38e 100644
--- a/apps/bff/src/modules/orders/services/checkout.service.ts
+++ b/apps/bff/src/modules/orders/services/checkout.service.ts
@@ -44,7 +44,7 @@ export class CheckoutService {
private summarizeSelectionsForLog(selections: OrderSelections): Record {
const addons = this.collectAddonRefs(selections);
const normalizeBool = (value?: string) =>
- value === "true" ? true : (value === "false" ? false : undefined);
+ value === "true" ? true : value === "false" ? false : undefined;
return {
planSku: selections.planSku,
diff --git a/apps/bff/src/modules/orders/services/order-validator.service.ts b/apps/bff/src/modules/orders/services/order-validator.service.ts
index 1f49ccd6..085b3b69 100644
--- a/apps/bff/src/modules/orders/services/order-validator.service.ts
+++ b/apps/bff/src/modules/orders/services/order-validator.service.ts
@@ -84,9 +84,9 @@ export class OrderValidator {
const productContainer = products.products?.product;
const existing = Array.isArray(productContainer)
? productContainer
- : (productContainer
+ : productContainer
? [productContainer]
- : []);
+ : [];
// Check for active Internet products
const activeInternetProducts = existing.filter((product: WhmcsProduct) => {
diff --git a/apps/bff/src/modules/orders/validators/internet-order.validator.ts b/apps/bff/src/modules/orders/validators/internet-order.validator.ts
index 4c99d6a8..38adee14 100644
--- a/apps/bff/src/modules/orders/validators/internet-order.validator.ts
+++ b/apps/bff/src/modules/orders/validators/internet-order.validator.ts
@@ -77,9 +77,9 @@ export class InternetOrderValidator {
const productContainer = products.products?.product;
const existing = Array.isArray(productContainer)
? productContainer
- : (productContainer
+ : productContainer
? [productContainer]
- : []);
+ : [];
// Check for active Internet products
const activeInternetProducts = existing.filter((product: WhmcsProduct) => {
diff --git a/apps/bff/src/modules/shared/workflow/workflow-case-manager.service.ts b/apps/bff/src/modules/shared/workflow/workflow-case-manager.service.ts
index fe756325..9182e686 100644
--- a/apps/bff/src/modules/shared/workflow/workflow-case-manager.service.ts
+++ b/apps/bff/src/modules/shared/workflow/workflow-case-manager.service.ts
@@ -52,9 +52,9 @@ export class WorkflowCaseManager {
: null;
const opportunityStatus = opportunityId
- ? (opportunityCreated
+ ? opportunityCreated
? "Created new opportunity for this order"
- : "Linked to existing opportunity")
+ : "Linked to existing opportunity"
: "No opportunity linked";
const description = this.buildDescription([
diff --git a/apps/bff/src/modules/subscriptions/internet-management/services/internet-cancellation.service.ts b/apps/bff/src/modules/subscriptions/internet-management/services/internet-cancellation.service.ts
index 4c755ffb..4568ee0d 100644
--- a/apps/bff/src/modules/subscriptions/internet-management/services/internet-cancellation.service.ts
+++ b/apps/bff/src/modules/subscriptions/internet-management/services/internet-cancellation.service.ts
@@ -77,9 +77,9 @@ export class InternetCancellationService {
const productContainer = productsResponse.products?.product;
const products = Array.isArray(productContainer)
? productContainer
- : (productContainer
+ : productContainer
? [productContainer]
- : []);
+ : [];
const subscription = products.find(
(p: { id?: number | string }) => Number(p.id) === subscriptionId
diff --git a/apps/bff/src/modules/subscriptions/sim-management/services/sim-validation.service.ts b/apps/bff/src/modules/subscriptions/sim-management/services/sim-validation.service.ts
index 15d5456c..fc5e25eb 100644
--- a/apps/bff/src/modules/subscriptions/sim-management/services/sim-validation.service.ts
+++ b/apps/bff/src/modules/subscriptions/sim-management/services/sim-validation.service.ts
@@ -107,9 +107,9 @@ export class SimValidationService {
// Account extraction result
extractedAccount,
accountSource: extractedAccount
- ? (subscription.domain
+ ? subscription.domain
? "domain field"
- : "custom field or order number")
+ : "custom field or order number"
: "NOT FOUND - check fields below",
// All custom fields for debugging
customFieldKeys: Object.keys(subscription.customFields || {}),
diff --git a/apps/bff/src/modules/verification/residence-card.service.ts b/apps/bff/src/modules/verification/residence-card.service.ts
index 769f893d..ab43549a 100644
--- a/apps/bff/src/modules/verification/residence-card.service.ts
+++ b/apps/bff/src/modules/verification/residence-card.service.ts
@@ -93,9 +93,9 @@ export class ResidenceCardService {
const reviewerNotes =
typeof rejectionRaw === "string" && rejectionRaw.trim().length > 0
? rejectionRaw.trim()
- : (typeof noteRaw === "string" && noteRaw.trim().length > 0
+ : typeof noteRaw === "string" && noteRaw.trim().length > 0
? noteRaw.trim()
- : null);
+ : null;
return residenceCardVerificationSchema.parse({
status,
diff --git a/apps/portal/next.config.mjs b/apps/portal/next.config.mjs
index 018d5b77..54e7685d 100644
--- a/apps/portal/next.config.mjs
+++ b/apps/portal/next.config.mjs
@@ -34,8 +34,11 @@ const nextConfig = {
config.watchOptions = {
...config.watchOptions,
ignored: config.watchOptions?.ignored
- ? [...(Array.isArray(config.watchOptions.ignored) ? config.watchOptions.ignored : [config.watchOptions.ignored])]
- .filter(p => !String(p).includes("packages/domain"))
+ ? [
+ ...(Array.isArray(config.watchOptions.ignored)
+ ? config.watchOptions.ignored
+ : [config.watchOptions.ignored]),
+ ].filter(p => !String(p).includes("packages/domain"))
: ["**/node_modules/**"],
};
// Add domain dist to snapshot managed paths for better change detection
diff --git a/apps/portal/src/components/molecules/OtpInput/OtpInput.tsx b/apps/portal/src/components/molecules/OtpInput/OtpInput.tsx
index 49a8eecc..9dafae15 100644
--- a/apps/portal/src/components/molecules/OtpInput/OtpInput.tsx
+++ b/apps/portal/src/components/molecules/OtpInput/OtpInput.tsx
@@ -149,9 +149,9 @@ export function OtpInput({
"disabled:opacity-50 disabled:cursor-not-allowed",
error
? "border-danger focus:ring-danger focus:border-danger"
- : (activeIndex === index
+ : activeIndex === index
? "border-primary"
- : "border-border hover:border-muted-foreground/50")
+ : "border-border hover:border-muted-foreground/50"
)}
aria-label={`Digit ${index + 1}`}
/>
diff --git a/apps/portal/src/components/organisms/AppShell/AppShell.tsx b/apps/portal/src/components/organisms/AppShell/AppShell.tsx
index d60ee067..cb991c0a 100644
--- a/apps/portal/src/components/organisms/AppShell/AppShell.tsx
+++ b/apps/portal/src/components/organisms/AppShell/AppShell.tsx
@@ -216,9 +216,9 @@ export function AppShell({ children }: AppShellProps) {
- ) : (isAuthReady ? (
+ ) : isAuthReady ? (
children
- ) : null)}
+ ) : null}
diff --git a/apps/portal/src/features/account/components/AddressCard.tsx b/apps/portal/src/features/account/components/AddressCard.tsx
index 95956178..56b9bd27 100644
--- a/apps/portal/src/features/account/components/AddressCard.tsx
+++ b/apps/portal/src/features/account/components/AddressCard.tsx
@@ -126,11 +126,11 @@ export function AddressCard({
)}
- ) : (hasAddress ? (
+ ) : hasAddress ? (
) : (
- ))}
+ )}
);
diff --git a/apps/portal/src/features/address/components/AddressStepJapan.tsx b/apps/portal/src/features/address/components/AddressStepJapan.tsx
index b040b6af..87528fb1 100644
--- a/apps/portal/src/features/address/components/AddressStepJapan.tsx
+++ b/apps/portal/src/features/address/components/AddressStepJapan.tsx
@@ -119,9 +119,9 @@ function fromLegacyFormat(address: LegacyAddressData): PartialJapanAddressFormDa
// For new users, leave it undefined so they must explicitly choose
const hasExistingAddress = address.postcode || address.state || address.city;
const residenceType = hasExistingAddress
- ? (roomNumber
+ ? roomNumber
? RESIDENCE_TYPE.APARTMENT
- : RESIDENCE_TYPE.HOUSE)
+ : RESIDENCE_TYPE.HOUSE
: undefined;
return {
diff --git a/apps/portal/src/features/address/components/JapanAddressForm.tsx b/apps/portal/src/features/address/components/JapanAddressForm.tsx
index cc011c58..f2b9ed41 100644
--- a/apps/portal/src/features/address/components/JapanAddressForm.tsx
+++ b/apps/portal/src/features/address/components/JapanAddressForm.tsx
@@ -336,9 +336,9 @@ export function JapanAddressForm({
required
helperText={
form.address.streetAddress.trim()
- ? (streetAddressError
+ ? streetAddressError
? undefined
- : "Valid format")
+ : "Valid format"
: "Enter chome-banchi-go (e.g., 1-5-3)"
}
>
diff --git a/apps/portal/src/features/address/components/ProgressIndicator.tsx b/apps/portal/src/features/address/components/ProgressIndicator.tsx
index c789349b..48a3f4a8 100644
--- a/apps/portal/src/features/address/components/ProgressIndicator.tsx
+++ b/apps/portal/src/features/address/components/ProgressIndicator.tsx
@@ -23,9 +23,9 @@ export function ProgressIndicator({ currentStep, totalSteps }: ProgressIndicator
"h-1 rounded-full transition-all duration-500",
i < currentStep
? "bg-primary flex-[2]"
- : (i === currentStep
+ : i === currentStep
? "bg-primary/40 flex-[2] animate-pulse"
- : "bg-border flex-1")
+ : "bg-border flex-1"
)}
/>
))}
diff --git a/apps/portal/src/features/auth/stores/auth.store.ts b/apps/portal/src/features/auth/stores/auth.store.ts
index 209adeed..0065c6ad 100644
--- a/apps/portal/src/features/auth/stores/auth.store.ts
+++ b/apps/portal/src/features/auth/stores/auth.store.ts
@@ -11,6 +11,7 @@ import {
authResponseSchema,
checkPasswordNeededResponseSchema,
loginOtpRequiredResponseSchema,
+ loginResponseSchema,
type AuthSession,
type CheckPasswordNeededResponse,
type LoginRequest,
@@ -40,8 +41,10 @@ export interface AuthState {
error: string | null;
hasCheckedAuth: boolean;
- // Two-step login with OTP
- initiateLogin: (credentials: LoginRequest) => Promise;
+ // Two-step login with OTP (or direct login in dev mode with SKIP_OTP)
+ initiateLogin: (
+ credentials: LoginRequest
+ ) => Promise;
verifyLoginOtp: (sessionToken: string, code: string) => Promise;
// Legacy login (kept for backward compatibility during migration)
login: (credentials: LoginRequest) => Promise;
@@ -195,6 +198,7 @@ export const useAuthStore = create()((set, get) => {
/**
* Step 1 of two-step login: Validate credentials and initiate OTP
* Returns OTP session info for the frontend to display OTP input
+ * In dev mode with SKIP_OTP=true, returns direct login result
*/
initiateLogin: async credentials => {
set({ loading: true, error: null });
@@ -203,12 +207,20 @@ export const useAuthStore = create()((set, get) => {
body: credentials,
disableCsrf: true, // Public auth endpoint, exempt from CSRF
});
- const parsed = loginOtpRequiredResponseSchema.safeParse(response.data);
+ const parsed = loginResponseSchema.safeParse(response.data);
if (!parsed.success) {
throw new Error(parsed.error.issues?.[0]?.message ?? "Login failed");
}
- set({ loading: false });
- return parsed.data;
+
+ // Check if OTP is required or if we got a direct login (dev mode)
+ if ("requiresOtp" in parsed.data && parsed.data.requiresOtp === true) {
+ set({ loading: false });
+ return parsed.data;
+ }
+
+ // Direct login - apply auth response and signal completion
+ applyAuthResponse(parsed.data as AuthResponseData);
+ return { requiresOtp: false as const };
} catch (error) {
const parsed = parseError(error);
set({ loading: false, error: parsed.message, isAuthenticated: false });
diff --git a/apps/portal/src/features/billing/views/PaymentMethods.tsx b/apps/portal/src/features/billing/views/PaymentMethods.tsx
index 3c912580..de1d5404 100644
--- a/apps/portal/src/features/billing/views/PaymentMethods.tsx
+++ b/apps/portal/src/features/billing/views/PaymentMethods.tsx
@@ -144,7 +144,7 @@ export function PaymentMethodsContainer() {
- ) : (paymentMethodsData && paymentMethodsData.paymentMethods.length > 0 ? (
+ ) : paymentMethodsData && paymentMethodsData.paymentMethods.length > 0 ? (
@@ -214,7 +214,7 @@ export function PaymentMethodsContainer() {
)}
- ))}
+ )}
diff --git a/apps/portal/src/features/checkout/components/CheckoutStatusBanners.tsx b/apps/portal/src/features/checkout/components/CheckoutStatusBanners.tsx
index c13b47fb..2e5b084b 100644
--- a/apps/portal/src/features/checkout/components/CheckoutStatusBanners.tsx
+++ b/apps/portal/src/features/checkout/components/CheckoutStatusBanners.tsx
@@ -132,11 +132,11 @@ export function CheckoutStatusBanners({
{eligibility.notes ? (
{eligibility.notes}
- ) : (eligibility.requestedAt ? (
+ ) : eligibility.requestedAt ? (
Last updated: {new Date(eligibility.requestedAt).toLocaleString()}
- ) : null)}
+ ) : null}
diff --git a/apps/portal/src/features/checkout/components/checkout-sections/IdentityVerificationSection.tsx b/apps/portal/src/features/checkout/components/checkout-sections/IdentityVerificationSection.tsx
index be52f925..e13bd4b2 100644
--- a/apps/portal/src/features/checkout/components/checkout-sections/IdentityVerificationSection.tsx
+++ b/apps/portal/src/features/checkout/components/checkout-sections/IdentityVerificationSection.tsx
@@ -254,11 +254,11 @@ function NotSubmittedContent({
Rejection note
{reviewerNotes}
- ) : (isRejected ? (
+ ) : isRejected ? (
Your document couldn't be approved. Please upload a new file to continue.
- ) : null)}
+ ) : null}
Upload a JPG, PNG, or PDF (max 5MB). We'll verify it before activating SIM service.
diff --git a/apps/portal/src/features/landing-page/views/PublicLandingView.tsx b/apps/portal/src/features/landing-page/views/PublicLandingView.tsx
index 0e79e182..8ae3395f 100644
--- a/apps/portal/src/features/landing-page/views/PublicLandingView.tsx
+++ b/apps/portal/src/features/landing-page/views/PublicLandingView.tsx
@@ -1341,14 +1341,14 @@ export function PublicLandingView() {
Sending...
>
- ) : (submitStatus === "success" ? (
+ ) : submitStatus === "success" ? (
<>
Sent!
>
) : (
"Submit"
- ))}
+ )}
diff --git a/apps/portal/src/features/notifications/components/NotificationDropdown.tsx b/apps/portal/src/features/notifications/components/NotificationDropdown.tsx
index 8862775e..930c7340 100644
--- a/apps/portal/src/features/notifications/components/NotificationDropdown.tsx
+++ b/apps/portal/src/features/notifications/components/NotificationDropdown.tsx
@@ -67,7 +67,7 @@ export const NotificationDropdown = memo(function NotificationDropdown({
- ) : (notifications.length === 0 ? (
+ ) : notifications.length === 0 ? (
No notifications yet
@@ -86,7 +86,7 @@ export const NotificationDropdown = memo(function NotificationDropdown({
/>
))}
- ))}
+ )}
{/* Footer */}
diff --git a/apps/portal/src/features/services/components/base/AddressForm.tsx b/apps/portal/src/features/services/components/base/AddressForm.tsx
index c28a6a41..4e3d72e2 100644
--- a/apps/portal/src/features/services/components/base/AddressForm.tsx
+++ b/apps/portal/src/features/services/components/base/AddressForm.tsx
@@ -287,9 +287,9 @@ export function AddressForm({
const containerClasses =
variant === "inline"
? ""
- : (variant === "compact"
+ : variant === "compact"
? "p-4 bg-gray-50 rounded-lg border border-gray-200"
- : "p-6 bg-white border border-gray-200 rounded-lg");
+ : "p-6 bg-white border border-gray-200 rounded-lg";
// Get all validation errors
const allErrors = Object.values(form.errors).filter(Boolean) as string[];
diff --git a/apps/portal/src/features/services/components/base/OrderSummary.tsx b/apps/portal/src/features/services/components/base/OrderSummary.tsx
index b1d8dc38..04b0958c 100644
--- a/apps/portal/src/features/services/components/base/OrderSummary.tsx
+++ b/apps/portal/src/features/services/components/base/OrderSummary.tsx
@@ -261,7 +261,7 @@ export function OrderSummary({
) : null}
>
- ) : (onContinue ? (
+ ) : onContinue ? (
- ) : null)}
+ ) : null}
)}
diff --git a/apps/portal/src/features/services/components/base/ProductCard.tsx b/apps/portal/src/features/services/components/base/ProductCard.tsx
index e39c45c6..feb73ea5 100644
--- a/apps/portal/src/features/services/components/base/ProductCard.tsx
+++ b/apps/portal/src/features/services/components/base/ProductCard.tsx
@@ -160,7 +160,7 @@ export function ProductCard({
>
{actionLabel}
- ) : (onClick ? (
+ ) : onClick ? (
- ) : null)}
+ ) : null}
{/* Custom footer */}
diff --git a/apps/portal/src/features/services/components/internet/InstallationOptions.tsx b/apps/portal/src/features/services/components/internet/InstallationOptions.tsx
index 25d5fbb7..1fb6944d 100644
--- a/apps/portal/src/features/services/components/internet/InstallationOptions.tsx
+++ b/apps/portal/src/features/services/components/internet/InstallationOptions.tsx
@@ -37,9 +37,9 @@ export function InstallationOptions({
installation.description ||
(installationTerm === "12-Month"
? "Spread the installation fee across 12 monthly payments."
- : (installationTerm === "24-Month"
+ : installationTerm === "24-Month"
? "Spread the installation fee across 24 monthly payments."
- : "Pay the full installation fee in one payment."));
+ : "Pay the full installation fee in one payment.");
return (
)}
- ) : (showNoResults ? (
+ ) : showNoResults ? (
@@ -269,7 +269,7 @@ export function DeviceCompatibility() {
to verify compatibility.
- ) : null)}
+ ) : null}
)}
diff --git a/apps/portal/src/features/services/components/sim/SimPlansContent.tsx b/apps/portal/src/features/services/components/sim/SimPlansContent.tsx
index f51eecf8..58dc87cb 100644
--- a/apps/portal/src/features/services/components/sim/SimPlansContent.tsx
+++ b/apps/portal/src/features/services/components/sim/SimPlansContent.tsx
@@ -266,9 +266,9 @@ export function SimPlansContent({
const tabPlans =
activeTab === "data-voice"
? plansByType.DataSmsVoice
- : (activeTab === "data-only"
+ : activeTab === "data-only"
? plansByType.DataOnly
- : plansByType.VoiceOnly);
+ : plansByType.VoiceOnly;
const regularPlans = tabPlans.filter(p => !p.simHasFamilyDiscount);
const familyPlans = tabPlans.filter(p => p.simHasFamilyDiscount);
diff --git a/apps/portal/src/features/services/hooks/useConfigureParams.ts b/apps/portal/src/features/services/hooks/useConfigureParams.ts
index 9b74a6f3..dd71dba3 100644
--- a/apps/portal/src/features/services/hooks/useConfigureParams.ts
+++ b/apps/portal/src/features/services/hooks/useConfigureParams.ts
@@ -64,9 +64,9 @@ export function useInternetConfigureParams() {
.split(",")
.map(s => s.trim())
.filter(Boolean)
- : (addonSkuParams.length > 0
+ : addonSkuParams.length > 0
? addonSkuParams
- : []);
+ : [];
return {
accessMode,
diff --git a/apps/portal/src/features/services/views/InternetEligibilityRequest.tsx b/apps/portal/src/features/services/views/InternetEligibilityRequest.tsx
index 619e28d1..9a1bf1e4 100644
--- a/apps/portal/src/features/services/views/InternetEligibilityRequest.tsx
+++ b/apps/portal/src/features/services/views/InternetEligibilityRequest.tsx
@@ -204,7 +204,7 @@ export function InternetEligibilityRequestView() {
{planLoading ? (
Loading selected plan…
- ) : (plan ? (
+ ) : plan ? (
Selected plan
@@ -212,7 +212,7 @@ export function InternetEligibilityRequestView() {
- ) : null)}
+ ) : null}
diff --git a/apps/portal/src/features/services/views/PublicEligibilityCheck.tsx b/apps/portal/src/features/services/views/PublicEligibilityCheck.tsx
index e17a069f..aa7c2de0 100644
--- a/apps/portal/src/features/services/views/PublicEligibilityCheck.tsx
+++ b/apps/portal/src/features/services/views/PublicEligibilityCheck.tsx
@@ -68,9 +68,9 @@ export function PublicEligibilityCheckView() {
step === "success" ? (hasAccount ? "Account Created" : "Request Submitted") : currentMeta.title;
const description =
step === "success"
- ? (hasAccount
+ ? hasAccount
? "Your account is ready and eligibility check is in progress."
- : "Your availability check request has been submitted.")
+ : "Your availability check request has been submitted."
: currentMeta.description;
return (
diff --git a/apps/portal/src/features/services/views/PublicInternetPlans.tsx b/apps/portal/src/features/services/views/PublicInternetPlans.tsx
index a9157a83..76b5d9fe 100644
--- a/apps/portal/src/features/services/views/PublicInternetPlans.tsx
+++ b/apps/portal/src/features/services/views/PublicInternetPlans.tsx
@@ -977,7 +977,7 @@ export function PublicInternetPlansContent({
{isLoading ? (
- ) : (consolidatedPlanData ? (
+ ) : consolidatedPlanData ? (
- ) : null)}
+ ) : null}
{/* Available Plans - Expandable cards by offering type */}
diff --git a/apps/portal/src/features/subscriptions/components/sim/ReissueSimModal.tsx b/apps/portal/src/features/subscriptions/components/sim/ReissueSimModal.tsx
index bd68e789..d6b9a978 100644
--- a/apps/portal/src/features/subscriptions/components/sim/ReissueSimModal.tsx
+++ b/apps/portal/src/features/subscriptions/components/sim/ReissueSimModal.tsx
@@ -79,9 +79,9 @@ export function ReissueSimModal({
} catch (error: unknown) {
const message =
process.env.NODE_ENV === "development"
- ? (error instanceof Error
+ ? error instanceof Error
? error.message
- : "Failed to submit reissue request")
+ : "Failed to submit reissue request"
: "Failed to submit reissue request. Please try again.";
onError(message);
} finally {
diff --git a/apps/portal/src/features/subscriptions/components/sim/SimActions.tsx b/apps/portal/src/features/subscriptions/components/sim/SimActions.tsx
index 208ec8ef..8319b210 100644
--- a/apps/portal/src/features/subscriptions/components/sim/SimActions.tsx
+++ b/apps/portal/src/features/subscriptions/components/sim/SimActions.tsx
@@ -388,9 +388,9 @@ function useSimActionsState(subscriptionId: number, onCancelSuccess?: () => void
} catch (err: unknown) {
setError(
process.env.NODE_ENV === "development"
- ? (err instanceof Error
+ ? err instanceof Error
? err.message
- : "Failed to cancel SIM service")
+ : "Failed to cancel SIM service"
: "Unable to cancel SIM service right now. Please try again."
);
} finally {
diff --git a/apps/portal/src/features/subscriptions/components/sim/SimManagementSection.tsx b/apps/portal/src/features/subscriptions/components/sim/SimManagementSection.tsx
index a2e5d32c..adcec8df 100644
--- a/apps/portal/src/features/subscriptions/components/sim/SimManagementSection.tsx
+++ b/apps/portal/src/features/subscriptions/components/sim/SimManagementSection.tsx
@@ -108,9 +108,9 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
} else {
setError(
process.env.NODE_ENV === "development"
- ? (err instanceof Error
+ ? err instanceof Error
? err.message
- : "Failed to load SIM information")
+ : "Failed to load SIM information"
: "Unable to load SIM information right now. Please try again."
);
}
@@ -252,9 +252,9 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
const remainingMB = simInfo.details.remainingQuotaMb.toFixed(1);
const usedMB = simInfo.usage?.monthlyUsageMb
? simInfo.usage.monthlyUsageMb.toFixed(2)
- : (simInfo.usage?.todayUsageMb
+ : simInfo.usage?.todayUsageMb
? simInfo.usage.todayUsageMb.toFixed(2)
- : "0.00");
+ : "0.00";
// Calculate percentage for circle
const totalMB = Number.parseFloat(remainingMB) + Number.parseFloat(usedMB);
diff --git a/apps/portal/src/features/subscriptions/views/CancelSubscription.tsx b/apps/portal/src/features/subscriptions/views/CancelSubscription.tsx
index 80d4c72f..f7c784a7 100644
--- a/apps/portal/src/features/subscriptions/views/CancelSubscription.tsx
+++ b/apps/portal/src/features/subscriptions/views/CancelSubscription.tsx
@@ -140,9 +140,9 @@ export function CancelSubscriptionContainer() {
} catch (e: unknown) {
setError(
process.env.NODE_ENV === "development"
- ? (e instanceof Error
+ ? e instanceof Error
? e.message
- : "Failed to load cancellation information")
+ : "Failed to load cancellation information"
: "Unable to load cancellation information right now. Please try again."
);
} finally {
@@ -176,9 +176,9 @@ export function CancelSubscriptionContainer() {
} catch (e: unknown) {
setFormError(
process.env.NODE_ENV === "development"
- ? (e instanceof Error
+ ? e instanceof Error
? e.message
- : "Failed to submit cancellation")
+ : "Failed to submit cancellation"
: "Unable to submit your cancellation right now. Please try again."
);
} finally {
diff --git a/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx b/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx
index 049cbb89..7a4f84bb 100644
--- a/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx
+++ b/apps/portal/src/features/subscriptions/views/SimChangePlan.tsx
@@ -32,9 +32,9 @@ export function SimChangePlanContainer() {
} catch (e: unknown) {
setError(
process.env.NODE_ENV === "development"
- ? (e instanceof Error
+ ? e instanceof Error
? e.message
- : "Failed to load available plans")
+ : "Failed to load available plans"
: "Unable to load available plans right now. Please try again."
);
} finally {
@@ -66,9 +66,9 @@ export function SimChangePlanContainer() {
} catch (e: unknown) {
setError(
process.env.NODE_ENV === "development"
- ? (e instanceof Error
+ ? e instanceof Error
? e.message
- : "Failed to change plan")
+ : "Failed to change plan"
: "Unable to submit your plan change right now. Please try again."
);
} finally {
diff --git a/apps/portal/src/features/subscriptions/views/SimReissue.tsx b/apps/portal/src/features/subscriptions/views/SimReissue.tsx
index 7d9e5725..cc035aaa 100644
--- a/apps/portal/src/features/subscriptions/views/SimReissue.tsx
+++ b/apps/portal/src/features/subscriptions/views/SimReissue.tsx
@@ -37,9 +37,9 @@ export function SimReissueContainer() {
} catch (e: unknown) {
setError(
process.env.NODE_ENV === "development"
- ? (e instanceof Error
+ ? e instanceof Error
? e.message
- : "Failed to load SIM details")
+ : "Failed to load SIM details"
: "Unable to load SIM details right now. Please try again."
);
} finally {
@@ -85,9 +85,9 @@ export function SimReissueContainer() {
} catch (e: unknown) {
setError(
process.env.NODE_ENV === "development"
- ? (e instanceof Error
+ ? e instanceof Error
? e.message
- : "Failed to submit reissue request")
+ : "Failed to submit reissue request"
: "Unable to submit your request right now. Please try again."
);
} finally {
diff --git a/apps/portal/src/features/subscriptions/views/SimTopUp.tsx b/apps/portal/src/features/subscriptions/views/SimTopUp.tsx
index 6b86ed5d..2d94784c 100644
--- a/apps/portal/src/features/subscriptions/views/SimTopUp.tsx
+++ b/apps/portal/src/features/subscriptions/views/SimTopUp.tsx
@@ -54,9 +54,9 @@ export function SimTopUpContainer() {
} catch (e: unknown) {
setError(
process.env.NODE_ENV === "development"
- ? (e instanceof Error
+ ? e instanceof Error
? e.message
- : "Failed to submit top-up")
+ : "Failed to submit top-up"
: "Unable to submit your top-up right now. Please try again."
);
} finally {
diff --git a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx
index 83c53472..8488afcd 100644
--- a/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx
+++ b/apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx
@@ -57,9 +57,9 @@ export function SubscriptionDetailContainer() {
// Show error message (only when we have an error, not during loading)
const pageError = error
- ? (process.env.NODE_ENV === "development" && error instanceof Error
+ ? process.env.NODE_ENV === "development" && error instanceof Error
? error.message
- : "Unable to load subscription details. Please try again.")
+ : "Unable to load subscription details. Please try again."
: null;
const productNameLower = subscription?.productName?.toLowerCase() ?? "";
diff --git a/apps/portal/src/features/support/views/NewSupportCaseView.tsx b/apps/portal/src/features/support/views/NewSupportCaseView.tsx
index 6867d4fd..991cb24d 100644
--- a/apps/portal/src/features/support/views/NewSupportCaseView.tsx
+++ b/apps/portal/src/features/support/views/NewSupportCaseView.tsx
@@ -42,9 +42,9 @@ export function NewSupportCaseView() {
} catch (err) {
setError(
process.env.NODE_ENV === "development"
- ? (err instanceof Error
+ ? err instanceof Error
? err.message
- : "Failed to create support case")
+ : "Failed to create support case"
: "Unable to create your support case right now. Please try again."
);
}
diff --git a/apps/portal/src/features/support/views/SupportCasesView.tsx b/apps/portal/src/features/support/views/SupportCasesView.tsx
index b61e3038..2d3bb047 100644
--- a/apps/portal/src/features/support/views/SupportCasesView.tsx
+++ b/apps/portal/src/features/support/views/SupportCasesView.tsx
@@ -212,7 +212,7 @@ export function SupportCasesView() {
))}
- ) : (hasActiveFilters ? (
+ ) : hasActiveFilters ? (
@@ -228,7 +228,7 @@ export function SupportCasesView() {
}}
/>
- ))}
+ )}
);
}
diff --git a/docs/development/auth/development-setup.md b/docs/development/auth/development-setup.md
index 7ca9fb8d..90630f72 100644
--- a/docs/development/auth/development-setup.md
+++ b/docs/development/auth/development-setup.md
@@ -29,6 +29,9 @@ DISABLE_CSRF=true
DISABLE_RATE_LIMIT=true
DISABLE_ACCOUNT_LOCKING=true
+# Skip OTP verification during login (direct login after credentials)
+SKIP_OTP=true
+
# Show detailed validation errors in responses
EXPOSE_VALIDATION_ERRORS=true
diff --git a/docs/refactoring/orchestrator-refactoring-plan.md b/docs/refactoring/orchestrator-refactoring-plan.md
deleted file mode 100644
index a828cce6..00000000
--- a/docs/refactoring/orchestrator-refactoring-plan.md
+++ /dev/null
@@ -1,729 +0,0 @@
-# Orchestrator Refactoring Plan
-
-## Overview
-
-This document outlines the refactoring plan for BFF orchestrators to align with enterprise-grade code structure patterns used by companies like Google, Amazon, and Microsoft.
-
-**Date**: 2026-02-03
-**Branch**: alt-design (post-merge from main)
-
----
-
-## Orchestrator Assessment Summary
-
-| Orchestrator | Lines | Status | Priority |
-| ------------------------------------------- | ----- | -------------------- | -------- |
-| `order-fulfillment-orchestrator.service.ts` | ~990 | ❌ Needs Refactoring | **HIGH** |
-| `subscriptions-orchestrator.service.ts` | ~475 | ⚠️ Minor Issues | LOW |
-| `auth-orchestrator.service.ts` | ~324 | ✅ Mostly Good | LOW |
-| `order-orchestrator.service.ts` | ~270 | ✅ Good | NONE |
-| `sim-orchestrator.service.ts` | ~200 | ✅ Excellent | NONE |
-| `billing-orchestrator.service.ts` | ~47 | ✅ Perfect | NONE |
-
----
-
-## HIGH Priority: Order Fulfillment Orchestrator
-
-### Current Problems
-
-1. **Inline Anonymous Functions in Distributed Transaction** (lines 172-557)
- - 10 transaction steps defined as inline arrow functions
- - Each step contains 20-80 lines of business logic
- - Violates Single Responsibility Principle
- - Difficult to unit test individual steps
-
-2. **File Size**: ~990 lines (should be < 300 for an orchestrator)
-
-3. **Mixed Concerns**: Helper methods embedded in orchestrator
- - `extractConfigurations()` (60+ lines) - data extraction
- - `extractContactIdentity()` (50+ lines) - data extraction
- - `formatBirthdayToYYYYMMDD()` (30 lines) - date formatting
-
-4. **Duplicate Logic**: Step tracking pattern repeated in each step
-
-### Target Architecture
-
-```
-apps/bff/src/modules/orders/
-├── services/
-│ ├── order-fulfillment-orchestrator.service.ts (~200 lines)
-│ ├── order-fulfillment-steps/
-│ │ ├── index.ts
-│ │ ├── validation.step.ts
-│ │ ├── sf-status-update.step.ts
-│ │ ├── sim-fulfillment.step.ts
-│ │ ├── sf-activated-update.step.ts
-│ │ ├── whmcs-mapping.step.ts
-│ │ ├── whmcs-create.step.ts
-│ │ ├── whmcs-accept.step.ts
-│ │ ├── sf-registration-complete.step.ts
-│ │ └── opportunity-update.step.ts
-│ └── mappers/
-│ └── order-configuration.mapper.ts
-```
-
-### Refactoring Steps
-
-#### Step 1: Create Step Interface and Base Class
-
-```typescript
-// apps/bff/src/modules/orders/services/order-fulfillment-steps/fulfillment-step.interface.ts
-
-import type { OrderFulfillmentContext } from "../order-fulfillment-orchestrator.service.js";
-import type { TransactionStep } from "@bff/infra/database/services/distributed-transaction.service.js";
-
-export interface FulfillmentStepConfig {
- context: OrderFulfillmentContext;
- logger: Logger;
-}
-
-export interface FulfillmentStep {
- readonly id: string;
- readonly description: string;
- readonly critical: boolean;
-
- /**
- * Build the transaction step for the distributed transaction
- */
- build(config: FulfillmentStepConfig): TransactionStep;
-
- /**
- * Check if this step should be included based on context
- */
- shouldInclude(context: OrderFulfillmentContext): boolean;
-}
-```
-
-#### Step 2: Extract Each Step to Its Own Class
-
-**Example: SIM Fulfillment Step**
-
-```typescript
-// apps/bff/src/modules/orders/services/order-fulfillment-steps/sim-fulfillment.step.ts
-
-import { Injectable, Inject } from "@nestjs/common";
-import { Logger } from "nestjs-pino";
-import type { FulfillmentStep, FulfillmentStepConfig } from "./fulfillment-step.interface.js";
-import type { OrderFulfillmentContext } from "../order-fulfillment-orchestrator.service.js";
-import { SimFulfillmentService } from "../sim-fulfillment.service.js";
-import { OrderConfigurationMapper } from "../mappers/order-configuration.mapper.js";
-import { extractErrorMessage } from "@bff/core/utils/error.util.js";
-
-@Injectable()
-export class SimFulfillmentStep implements FulfillmentStep {
- readonly id = "sim_fulfillment";
- readonly description = "SIM activation via Freebit (PA05-18 + PA02-01 + PA05-05)";
- readonly critical = true; // Set dynamically based on SIM type
-
- constructor(
- private readonly simFulfillmentService: SimFulfillmentService,
- private readonly configMapper: OrderConfigurationMapper,
- @Inject(Logger) private readonly logger: Logger
- ) {}
-
- shouldInclude(context: OrderFulfillmentContext): boolean {
- return context.orderDetails?.orderType === "SIM";
- }
-
- build(config: FulfillmentStepConfig) {
- const { context } = config;
-
- return {
- id: this.id,
- description: this.description,
- execute: async () => this.execute(context),
- rollback: async () => this.rollback(context),
- critical: context.validation?.sfOrder?.SIM_Type__c === "Physical SIM",
- };
- }
-
- private async execute(context: OrderFulfillmentContext) {
- if (context.orderDetails?.orderType !== "SIM") {
- return { activated: false, simType: "eSIM" as const };
- }
-
- const sfOrder = context.validation?.sfOrder;
- const configurations = this.configMapper.extractConfigurations(
- context.payload?.configurations,
- sfOrder
- );
-
- const assignedPhysicalSimId = this.extractAssignedSimId(sfOrder);
- const voiceMailEnabled = sfOrder?.SIM_Voice_Mail__c === true;
- const callWaitingEnabled = sfOrder?.SIM_Call_Waiting__c === true;
- const contactIdentity = this.configMapper.extractContactIdentity(sfOrder);
-
- this.logger.log("Starting SIM fulfillment (before WHMCS)", {
- orderId: context.sfOrderId,
- simType: sfOrder?.SIM_Type__c,
- assignedPhysicalSimId,
- voiceMailEnabled,
- callWaitingEnabled,
- hasContactIdentity: !!contactIdentity,
- });
-
- const result = await this.simFulfillmentService.fulfillSimOrder({
- orderDetails: context.orderDetails,
- configurations,
- assignedPhysicalSimId,
- voiceMailEnabled,
- callWaitingEnabled,
- contactIdentity,
- });
-
- context.simFulfillmentResult = result;
- return result;
- }
-
- private async rollback(context: OrderFulfillmentContext) {
- this.logger.warn("SIM fulfillment step needs rollback - manual intervention may be required", {
- sfOrderId: context.sfOrderId,
- simFulfillmentResult: context.simFulfillmentResult,
- });
- }
-
- private extractAssignedSimId(sfOrder?: SalesforceOrderRecord | null): string | undefined {
- return typeof sfOrder?.Assign_Physical_SIM__c === "string"
- ? sfOrder.Assign_Physical_SIM__c
- : undefined;
- }
-}
-```
-
-#### Step 3: Extract Configuration Mapper
-
-```typescript
-// apps/bff/src/modules/orders/services/mappers/order-configuration.mapper.ts
-
-import { Injectable } from "@nestjs/common";
-import type { SalesforceOrderRecord } from "@customer-portal/domain/orders/providers";
-import type { ContactIdentityData } from "../sim-fulfillment.service.js";
-
-@Injectable()
-export class OrderConfigurationMapper {
- /**
- * Extract and normalize configurations from payload and Salesforce order
- */
- extractConfigurations(
- rawConfigurations: unknown,
- sfOrder?: SalesforceOrderRecord | null
- ): Record {
- const config: Record = {};
-
- if (rawConfigurations && typeof rawConfigurations === "object") {
- Object.assign(config, rawConfigurations as Record);
- }
-
- if (sfOrder) {
- this.fillFromSalesforceOrder(config, sfOrder);
- }
-
- return config;
- }
-
- /**
- * Extract contact identity data from Salesforce order porting fields
- */
- extractContactIdentity(sfOrder?: SalesforceOrderRecord | null): ContactIdentityData | undefined {
- if (!sfOrder) return undefined;
-
- const firstnameKanji = sfOrder.Porting_FirstName__c;
- const lastnameKanji = sfOrder.Porting_LastName__c;
- const firstnameKana = sfOrder.Porting_FirstName_Katakana__c;
- const lastnameKana = sfOrder.Porting_LastName_Katakana__c;
- const genderRaw = sfOrder.Porting_Gender__c;
- const birthdayRaw = sfOrder.Porting_DateOfBirth__c;
-
- if (!this.validateNameFields(firstnameKanji, lastnameKanji, firstnameKana, lastnameKana)) {
- return undefined;
- }
-
- const gender = this.validateGender(genderRaw);
- if (!gender) return undefined;
-
- const birthday = this.formatBirthdayToYYYYMMDD(birthdayRaw);
- if (!birthday) return undefined;
-
- return {
- firstnameKanji: firstnameKanji!,
- lastnameKanji: lastnameKanji!,
- firstnameKana: firstnameKana!,
- lastnameKana: lastnameKana!,
- gender,
- birthday,
- };
- }
-
- /**
- * Format birthday from various formats to YYYYMMDD
- */
- formatBirthdayToYYYYMMDD(dateStr?: string | null): string | undefined {
- if (!dateStr) return undefined;
-
- if (/^\d{8}$/.test(dateStr)) {
- return dateStr;
- }
-
- const isoMatch = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
- if (isoMatch) {
- return `${isoMatch[1]}${isoMatch[2]}${isoMatch[3]}`;
- }
-
- try {
- const date = new Date(dateStr);
- if (!isNaN(date.getTime())) {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, "0");
- const day = String(date.getDate()).padStart(2, "0");
- return `${year}${month}${day}`;
- }
- } catch {
- // Parsing failed
- }
-
- return undefined;
- }
-
- private fillFromSalesforceOrder(
- config: Record,
- sfOrder: SalesforceOrderRecord
- ): void {
- const fieldMappings: Array<[string, keyof SalesforceOrderRecord]> = [
- ["simType", "SIM_Type__c"],
- ["eid", "EID__c"],
- ["activationType", "Activation_Type__c"],
- ["scheduledAt", "Activation_Scheduled_At__c"],
- ["mnpPhone", "MNP_Phone_Number__c"],
- ["mnpNumber", "MNP_Reservation_Number__c"],
- ["mnpExpiry", "MNP_Expiry_Date__c"],
- ["mvnoAccountNumber", "MVNO_Account_Number__c"],
- ["portingFirstName", "Porting_FirstName__c"],
- ["portingLastName", "Porting_LastName__c"],
- ["portingFirstNameKatakana", "Porting_FirstName_Katakana__c"],
- ["portingLastNameKatakana", "Porting_LastName_Katakana__c"],
- ["portingGender", "Porting_Gender__c"],
- ["portingDateOfBirth", "Porting_DateOfBirth__c"],
- ];
-
- for (const [configKey, sfField] of fieldMappings) {
- if (!config[configKey] && sfOrder[sfField]) {
- config[configKey] = sfOrder[sfField];
- }
- }
-
- // Handle MNP flag specially
- if (!config["isMnp"] && sfOrder.MNP_Application__c) {
- config["isMnp"] = "true";
- }
- }
-
- private validateNameFields(
- firstnameKanji?: string | null,
- lastnameKanji?: string | null,
- firstnameKana?: string | null,
- lastnameKana?: string | null
- ): boolean {
- return !!(firstnameKanji && lastnameKanji && firstnameKana && lastnameKana);
- }
-
- private validateGender(genderRaw?: string | null): "M" | "F" | undefined {
- return genderRaw === "M" || genderRaw === "F" ? genderRaw : undefined;
- }
-}
-```
-
-#### Step 4: Create Step Builder Service
-
-```typescript
-// apps/bff/src/modules/orders/services/order-fulfillment-steps/fulfillment-step-builder.service.ts
-
-import { Injectable, Inject } from "@nestjs/common";
-import { Logger } from "nestjs-pino";
-import type { TransactionStep } from "@bff/infra/database/services/distributed-transaction.service.js";
-import type { OrderFulfillmentContext } from "../order-fulfillment-orchestrator.service.js";
-
-// Import all steps
-import { ValidationStep } from "./validation.step.js";
-import { SfStatusUpdateStep } from "./sf-status-update.step.js";
-import { SimFulfillmentStep } from "./sim-fulfillment.step.js";
-import { SfActivatedUpdateStep } from "./sf-activated-update.step.js";
-import { WhmcsMappingStep } from "./whmcs-mapping.step.js";
-import { WhmcsCreateStep } from "./whmcs-create.step.js";
-import { WhmcsAcceptStep } from "./whmcs-accept.step.js";
-import { SfRegistrationCompleteStep } from "./sf-registration-complete.step.js";
-import { OpportunityUpdateStep } from "./opportunity-update.step.js";
-
-@Injectable()
-export class FulfillmentStepBuilder {
- private readonly allSteps: FulfillmentStep[];
-
- constructor(
- validationStep: ValidationStep,
- sfStatusUpdateStep: SfStatusUpdateStep,
- simFulfillmentStep: SimFulfillmentStep,
- sfActivatedUpdateStep: SfActivatedUpdateStep,
- whmcsMappingStep: WhmcsMappingStep,
- whmcsCreateStep: WhmcsCreateStep,
- whmcsAcceptStep: WhmcsAcceptStep,
- sfRegistrationCompleteStep: SfRegistrationCompleteStep,
- opportunityUpdateStep: OpportunityUpdateStep,
- @Inject(Logger) private readonly logger: Logger
- ) {
- // Define step execution order
- this.allSteps = [
- sfStatusUpdateStep,
- simFulfillmentStep,
- sfActivatedUpdateStep,
- whmcsMappingStep,
- whmcsCreateStep,
- whmcsAcceptStep,
- sfRegistrationCompleteStep,
- opportunityUpdateStep,
- ];
- }
-
- /**
- * Build transaction steps based on context
- */
- buildTransactionSteps(context: OrderFulfillmentContext): TransactionStep[] {
- const config = { context, logger: this.logger };
-
- return this.allSteps
- .filter(step => step.shouldInclude(context))
- .map(step => step.build(config));
- }
-}
-```
-
-#### Step 5: Refactor Orchestrator to Use Step Builder
-
-```typescript
-// apps/bff/src/modules/orders/services/order-fulfillment-orchestrator.service.ts (REFACTORED)
-
-import { Injectable, Inject } from "@nestjs/common";
-import { Logger } from "nestjs-pino";
-import { DistributedTransactionService } from "@bff/infra/database/services/distributed-transaction.service.js";
-import { FulfillmentStepBuilder } from "./order-fulfillment-steps/fulfillment-step-builder.service.js";
-import { OrderFulfillmentValidator } from "./order-fulfillment-validator.service.js";
-import { OrderFulfillmentErrorService } from "./order-fulfillment-error.service.js";
-import { OrderEventsService } from "./order-events.service.js";
-import { OrdersCacheService } from "./orders-cache.service.js";
-import { extractErrorMessage } from "@bff/core/utils/error.util.js";
-import { FulfillmentException } from "@bff/core/exceptions/domain-exceptions.js";
-
-// ... types remain the same ...
-
-@Injectable()
-export class OrderFulfillmentOrchestrator {
- constructor(
- @Inject(Logger) private readonly logger: Logger,
- private readonly stepBuilder: FulfillmentStepBuilder,
- private readonly validator: OrderFulfillmentValidator,
- private readonly errorService: OrderFulfillmentErrorService,
- private readonly transactionService: DistributedTransactionService,
- private readonly orderEvents: OrderEventsService,
- private readonly ordersCache: OrdersCacheService
- ) {}
-
- async executeFulfillment(
- sfOrderId: string,
- payload: Record,
- idempotencyKey: string
- ): Promise {
- const context = this.initializeContext(sfOrderId, idempotencyKey, payload);
-
- this.logger.log("Starting transactional fulfillment orchestration", {
- sfOrderId,
- idempotencyKey,
- });
-
- try {
- // Step 1: Validate
- const validation = await this.validator.validateFulfillmentRequest(sfOrderId, idempotencyKey);
- context.validation = validation;
-
- if (validation.isAlreadyProvisioned) {
- return this.handleAlreadyProvisioned(context);
- }
-
- // Step 2: Build and execute transaction steps
- const steps = this.stepBuilder.buildTransactionSteps(context);
- const result = await this.transactionService.executeDistributedTransaction(steps, {
- description: `Order fulfillment for ${sfOrderId}`,
- timeout: 300000,
- continueOnNonCriticalFailure: true,
- });
-
- if (!result.success) {
- throw new FulfillmentException(result.error || "Fulfillment transaction failed", {
- sfOrderId,
- idempotencyKey,
- stepsExecuted: result.stepsExecuted,
- stepsRolledBack: result.stepsRolledBack,
- });
- }
-
- this.logger.log("Transactional fulfillment completed successfully", {
- sfOrderId,
- stepsExecuted: result.stepsExecuted,
- duration: result.duration,
- });
-
- await this.invalidateCaches(context);
- return context;
- } catch (error) {
- await this.handleFulfillmentError(context, error as Error);
- throw error;
- }
- }
-
- private initializeContext(
- sfOrderId: string,
- idempotencyKey: string,
- payload: Record
- ): OrderFulfillmentContext {
- return {
- sfOrderId,
- idempotencyKey,
- validation: null,
- payload,
- steps: [],
- };
- }
-
- private handleAlreadyProvisioned(context: OrderFulfillmentContext): OrderFulfillmentContext {
- this.logger.log("Order already provisioned, skipping fulfillment", {
- sfOrderId: context.sfOrderId,
- });
- this.orderEvents.publish(context.sfOrderId, {
- orderId: context.sfOrderId,
- status: "Completed",
- activationStatus: "Activated",
- stage: "completed",
- source: "fulfillment",
- message: "Order already provisioned",
- timestamp: new Date().toISOString(),
- payload: { whmcsOrderId: context.validation?.whmcsOrderId },
- });
- return context;
- }
-
- private async invalidateCaches(context: OrderFulfillmentContext): Promise {
- const accountId = context.validation?.sfOrder?.AccountId;
- await Promise.all([
- this.ordersCache.invalidateOrder(context.sfOrderId),
- accountId ? this.ordersCache.invalidateAccountOrders(accountId) : Promise.resolve(),
- ]).catch(error => {
- this.logger.warn("Failed to invalidate caches", { error: extractErrorMessage(error) });
- });
- }
-
- private async handleFulfillmentError(
- context: OrderFulfillmentContext,
- error: Error
- ): Promise {
- await this.invalidateCaches(context);
- await this.errorService.handleAndReport(context, error);
- this.orderEvents.publishFailure(context.sfOrderId, error.message);
- }
-
- getFulfillmentSummary(context: OrderFulfillmentContext) {
- // ... same as before, no changes needed ...
- }
-}
-```
-
-#### Step 6: Update Module Registration
-
-```typescript
-// apps/bff/src/modules/orders/orders.module.ts
-
-// Add new providers
-import { FulfillmentStepBuilder } from "./services/order-fulfillment-steps/fulfillment-step-builder.service.js";
-import { OrderConfigurationMapper } from "./services/mappers/order-configuration.mapper.js";
-import {
- SfStatusUpdateStep,
- SimFulfillmentStep,
- SfActivatedUpdateStep,
- WhmcsMappingStep,
- WhmcsCreateStep,
- WhmcsAcceptStep,
- SfRegistrationCompleteStep,
- OpportunityUpdateStep,
-} from "./services/order-fulfillment-steps/index.js";
-
-@Module({
- providers: [
- // Existing services...
-
- // New step classes
- FulfillmentStepBuilder,
- OrderConfigurationMapper,
- SfStatusUpdateStep,
- SimFulfillmentStep,
- SfActivatedUpdateStep,
- WhmcsMappingStep,
- WhmcsCreateStep,
- WhmcsAcceptStep,
- SfRegistrationCompleteStep,
- OpportunityUpdateStep,
- ],
-})
-export class OrdersModule {}
-```
-
----
-
-## LOW Priority: Subscriptions Orchestrator
-
-### Current Issues
-
-1. Business logic in filter methods (lines 193-250):
- - `getExpiringSoon()` - date filtering logic
- - `getRecentActivity()` - date filtering logic
- - `searchSubscriptions()` - search logic
-
-2. Cache helper methods could be in a separate service
-
-### Recommended Changes
-
-1. Extract date filtering to a utility:
-
- ```typescript
- // @bff/core/utils/date-filter.util.ts
- export function filterByDateRange(
- items: T[],
- dateExtractor: (item: T) => Date,
- range: { start?: Date; end?: Date }
- ): T[] { ... }
- ```
-
-2. Consider extracting `SubscriptionFilterService` if filtering becomes more complex
-
-**Status**: Not urgent - current implementation is acceptable
-
----
-
-## LOW Priority: Auth Orchestrator
-
-### Current Issues
-
-1. `getAccountStatus()` method (lines 241-296) contains complex conditional logic
- - Could be extracted to `AccountStatusResolver` service
-
-### Recommended Changes
-
-```typescript
-// apps/bff/src/modules/auth/application/account-status-resolver.service.ts
-
-@Injectable()
-export class AccountStatusResolver {
- async resolve(email: string): Promise {
- // Move logic from getAccountStatus here
- }
-}
-```
-
-**Status**: Not urgent - method is well-contained and testable as-is
-
----
-
-## No Changes Needed
-
-### Order Orchestrator ✅
-
-- Clean thin delegation pattern
-- Appropriate size (~270 lines)
-- Single private method is justified
-
-### SIM Orchestrator ✅
-
-- Excellent thin delegation
-- All methods delegate to specialized services
-- `getSimInfo` has reasonable composition logic
-
-### Billing Orchestrator ✅
-
-- Perfect thin facade pattern
-- Pure delegation only
-- Model example for others
-
----
-
-## Implementation Checklist
-
-### Phase 1: High Priority (Order Fulfillment Orchestrator)
-
-- [ ] Create `order-fulfillment-steps/` directory
-- [ ] Create `fulfillment-step.interface.ts`
-- [ ] Create `OrderConfigurationMapper` service
-- [ ] Extract `SfStatusUpdateStep`
-- [ ] Extract `SimFulfillmentStep`
-- [ ] Extract `SfActivatedUpdateStep`
-- [ ] Extract `WhmcsMappingStep`
-- [ ] Extract `WhmcsCreateStep`
-- [ ] Extract `WhmcsAcceptStep`
-- [ ] Extract `SfRegistrationCompleteStep`
-- [ ] Extract `OpportunityUpdateStep`
-- [ ] Create `FulfillmentStepBuilder` service
-- [ ] Refactor `OrderFulfillmentOrchestrator` to use step builder
-- [ ] Update `OrdersModule` with new providers
-- [ ] Add unit tests for each step class
-- [ ] Add integration tests for step builder
-
-### Phase 2: Low Priority
-
-- [ ] Extract `SubscriptionFilterService` (if needed)
-- [ ] Extract `AccountStatusResolver` (if needed)
-
----
-
-## Testing Strategy
-
-### Step Classes
-
-Each step class should have unit tests that verify:
-
-1. `shouldInclude()` returns correct boolean based on context
-2. `execute()` performs expected operations
-3. `rollback()` handles cleanup appropriately
-4. Error cases are handled correctly
-
-### Step Builder
-
-Integration tests should verify:
-
-1. Correct steps are included for SIM orders
-2. Correct steps are included for non-SIM orders
-3. Step order is maintained
-4. Context is properly passed between steps
-
-### Orchestrator
-
-E2E tests should verify:
-
-1. Complete fulfillment flow works
-2. Partial failures trigger rollbacks
-3. Already provisioned orders are handled
-4. Error reporting works correctly
-
----
-
-## Benefits of This Refactoring
-
-1. **Testability**: Each step can be unit tested in isolation
-2. **Maintainability**: Changes to one step don't affect others
-3. **Readability**: Orchestrator becomes a thin coordinator
-4. **Extensibility**: New steps can be added without modifying orchestrator
-5. **Reusability**: Steps can potentially be reused in other workflows
-6. **Debugging**: Easier to identify which step failed
-7. **Code Review**: Smaller, focused PRs for each step
-
----
-
-## References
-
-- Google Engineering Practices: https://google.github.io/eng-practices/
-- Microsoft .NET Application Architecture: https://docs.microsoft.com/en-us/dotnet/architecture/
-- Clean Architecture by Robert C. Martin
-- Domain-Driven Design by Eric Evans
diff --git a/env/dev.env.sample b/env/dev.env.sample
index 6f42f45d..93fe6325 100644
--- a/env/dev.env.sample
+++ b/env/dev.env.sample
@@ -106,3 +106,4 @@ SENDGRID_API_KEY=
# DISABLE_CSRF=false
# DISABLE_RATE_LIMIT=false
# DISABLE_ACCOUNT_LOCKING=false
+# SKIP_OTP=false # Skip OTP verification during login (dev only)
diff --git a/packages/domain/opportunity/contract.ts b/packages/domain/opportunity/contract.ts
index ede2d94e..253199db 100644
--- a/packages/domain/opportunity/contract.ts
+++ b/packages/domain/opportunity/contract.ts
@@ -585,7 +585,7 @@ export function getOrderTrackingSteps(
return stages.map((s, index) => ({
label: s.label,
- status: index < currentStep ? "completed" : (index === currentStep ? "current" : "upcoming"),
+ status: index < currentStep ? "completed" : index === currentStep ? "current" : "upcoming",
}));
}
diff --git a/packages/domain/subscriptions/providers/whmcs/mapper.ts b/packages/domain/subscriptions/providers/whmcs/mapper.ts
index c2177e51..fab3566e 100644
--- a/packages/domain/subscriptions/providers/whmcs/mapper.ts
+++ b/packages/domain/subscriptions/providers/whmcs/mapper.ts
@@ -195,9 +195,9 @@ export function transformWhmcsSubscriptionListResponse(
const productContainer = parsed.products?.product;
const products = Array.isArray(productContainer)
? productContainer
- : (productContainer
+ : productContainer
? [productContainer]
- : []);
+ : [];
const subscriptions: Subscription[] = [];
for (const product of products) {
@@ -213,9 +213,9 @@ export function transformWhmcsSubscriptionListResponse(
const totalResults =
typeof totalResultsRaw === "number"
? totalResultsRaw
- : (typeof totalResultsRaw === "string"
+ : typeof totalResultsRaw === "string"
? Number.parseInt(totalResultsRaw, 10)
- : subscriptions.length);
+ : subscriptions.length;
if (status) {
const normalizedStatus = subscriptionStatusSchema.parse(status);