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.
This commit is contained in:
barsa 2026-02-03 18:28:38 +09:00
parent 2dec0af63b
commit 7abd433d95
52 changed files with 159 additions and 941 deletions

View File

@ -4,11 +4,11 @@
* Usage: node scripts/check-sim-status.mjs <phone_number> * Usage: node scripts/check-sim-status.mjs <phone_number>
*/ */
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_BASE_URL = "https://i1-q.mvno.net/emptool/api";
const FREEBIT_OEM_ID = 'PASI'; const FREEBIT_OEM_ID = "PASI";
const FREEBIT_OEM_KEY = '6Au3o7wrQNR07JxFHPmf0YfFqN9a31t5'; const FREEBIT_OEM_KEY = "6Au3o7wrQNR07JxFHPmf0YfFqN9a31t5";
async function getAuthKey() { async function getAuthKey() {
const request = { const request = {
@ -17,8 +17,8 @@ async function getAuthKey() {
}; };
const response = await fetch(`${FREEBIT_BASE_URL}/authOem/`, { const response = await fetch(`${FREEBIT_BASE_URL}/authOem/`, {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `json=${JSON.stringify(request)}`, body: `json=${JSON.stringify(request)}`,
}); });
@ -27,7 +27,7 @@ async function getAuthKey() {
} }
const data = await response.json(); 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)}`); 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 request = { authKey, account };
const response = await fetch(`${FREEBIT_BASE_URL}/mvno/getTrafficInfo/`, { const response = await fetch(`${FREEBIT_BASE_URL}/mvno/getTrafficInfo/`, {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `json=${JSON.stringify(request)}`, body: `json=${JSON.stringify(request)}`,
}); });
return response.json(); return response.json();
@ -48,13 +48,13 @@ async function getTrafficInfo(authKey, account) {
async function getAccountDetails(authKey, account) { async function getAccountDetails(authKey, account) {
const request = { const request = {
authKey, authKey,
version: '2', version: "2",
requestDatas: [{ kind: 'MVNO', account }], requestDatas: [{ kind: "MVNO", account }],
}; };
const response = await fetch(`${FREEBIT_BASE_URL}/master/getAcnt/`, { const response = await fetch(`${FREEBIT_BASE_URL}/master/getAcnt/`, {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `json=${JSON.stringify(request)}`, body: `json=${JSON.stringify(request)}`,
}); });
return response.json(); return response.json();
@ -65,20 +65,19 @@ async function main() {
try { try {
const authKey = await getAuthKey(); const authKey = await getAuthKey();
console.log('✓ Authenticated with Freebit\n'); console.log("✓ Authenticated with Freebit\n");
// Try getTrafficInfo first (simpler) // Try getTrafficInfo first (simpler)
console.log('--- Traffic Info (/mvno/getTrafficInfo/) ---'); console.log("--- Traffic Info (/mvno/getTrafficInfo/) ---");
const trafficInfo = await getTrafficInfo(authKey, account); const trafficInfo = await getTrafficInfo(authKey, account);
console.log(JSON.stringify(trafficInfo, null, 2)); console.log(JSON.stringify(trafficInfo, null, 2));
// Try getAcnt for full details // 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); const details = await getAccountDetails(authKey, account);
console.log(JSON.stringify(details, null, 2)); console.log(JSON.stringify(details, null, 2));
} catch (error) { } catch (error) {
console.error('❌ Error:', error.message); console.error("❌ Error:", error.message);
} }
} }

View File

@ -1,90 +1 @@
Timestamp,API Endpoint,API Method,Phone Number,SIM Identifier,Request Payload,Response Status,Error,Additional Info 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

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

View File

@ -9,6 +9,7 @@ export interface DevAuthConfig {
disableAccountLocking: boolean; disableAccountLocking: boolean;
enableDebugLogs: boolean; enableDebugLogs: boolean;
simplifiedErrorMessages: boolean; simplifiedErrorMessages: boolean;
skipOtp: boolean;
} }
export const createDevAuthConfig = (): DevAuthConfig => { export const createDevAuthConfig = (): DevAuthConfig => {
@ -29,6 +30,9 @@ export const createDevAuthConfig = (): DevAuthConfig => {
// Show detailed error messages in development // Show detailed error messages in development
simplifiedErrorMessages: isDevelopment, simplifiedErrorMessages: isDevelopment,
// Skip OTP verification in development (login directly after credentials)
skipOtp: isDevelopment && process.env["SKIP_OTP"] === "true",
}; };
}; };

View File

@ -232,9 +232,9 @@ export class UnifiedExceptionFilter implements ExceptionFilter {
userAgent: userAgent:
typeof userAgentHeader === "string" typeof userAgentHeader === "string"
? userAgentHeader ? userAgentHeader
: (Array.isArray(userAgentHeader) : Array.isArray(userAgentHeader)
? userAgentHeader[0] ? userAgentHeader[0]
: undefined), : undefined,
ip: request.ip, ip: request.ip,
}; };
} }

View File

@ -354,17 +354,17 @@ export class FreebitClientService {
const timestamp = this.testTracker.getCurrentTimestamp(); const timestamp = this.testTracker.getCurrentTimestamp();
const resultCode = response?.resultCode const resultCode = response?.resultCode
? String(response.resultCode) ? String(response.resultCode)
: (error instanceof FreebitError : error instanceof FreebitError
? String(error.resultCode || "ERROR") ? String(error.resultCode || "ERROR")
: "ERROR"); : "ERROR";
const statusMessage = const statusMessage =
response?.status?.message || response?.status?.message ||
(error instanceof FreebitError (error instanceof FreebitError
? error.message ? error.message
: (error : error
? extractErrorMessage(error) ? extractErrorMessage(error)
: "Success")); : "Success");
await this.testTracker.logApiCall({ await this.testTracker.logApiCall({
timestamp, timestamp,

View File

@ -36,6 +36,7 @@ import {
REFRESH_COOKIE_PATH, REFRESH_COOKIE_PATH,
TOKEN_TYPE, TOKEN_TYPE,
} from "./utils/auth-cookie.util.js"; } from "./utils/auth-cookie.util.js";
import { devAuthConfig } from "@bff/core/config/auth-dev.config.js";
// Import Zod schemas from domain // Import Zod schemas from domain
import { import {
@ -130,6 +131,21 @@ export class AuthController {
@Req() req: RequestWithUser & RequestWithRateLimit, @Req() req: RequestWithUser & RequestWithRateLimit,
@Res({ passthrough: true }) res: Response @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 // Credentials validated by LocalAuthGuard - now initiate OTP
const fingerprint = getRequestFingerprint(req); const fingerprint = getRequestFingerprint(req);
const otpResult = await this.loginOtpWorkflow.initiateOtp( const otpResult = await this.loginOtpWorkflow.initiateOtp(
@ -141,8 +157,6 @@ export class AuthController {
fingerprint fingerprint
); );
this.applyAuthRateLimitHeaders(req, res);
// Return OTP required response - no tokens issued yet // Return OTP required response - no tokens issued yet
return { return {
requiresOtp: true, requiresOtp: true,

View File

@ -44,7 +44,7 @@ export class CheckoutService {
private summarizeSelectionsForLog(selections: OrderSelections): Record<string, unknown> { private summarizeSelectionsForLog(selections: OrderSelections): Record<string, unknown> {
const addons = this.collectAddonRefs(selections); const addons = this.collectAddonRefs(selections);
const normalizeBool = (value?: string) => const normalizeBool = (value?: string) =>
value === "true" ? true : (value === "false" ? false : undefined); value === "true" ? true : value === "false" ? false : undefined;
return { return {
planSku: selections.planSku, planSku: selections.planSku,

View File

@ -84,9 +84,9 @@ export class OrderValidator {
const productContainer = products.products?.product; const productContainer = products.products?.product;
const existing = Array.isArray(productContainer) const existing = Array.isArray(productContainer)
? productContainer ? productContainer
: (productContainer : productContainer
? [productContainer] ? [productContainer]
: []); : [];
// Check for active Internet products // Check for active Internet products
const activeInternetProducts = existing.filter((product: WhmcsProduct) => { const activeInternetProducts = existing.filter((product: WhmcsProduct) => {

View File

@ -77,9 +77,9 @@ export class InternetOrderValidator {
const productContainer = products.products?.product; const productContainer = products.products?.product;
const existing = Array.isArray(productContainer) const existing = Array.isArray(productContainer)
? productContainer ? productContainer
: (productContainer : productContainer
? [productContainer] ? [productContainer]
: []); : [];
// Check for active Internet products // Check for active Internet products
const activeInternetProducts = existing.filter((product: WhmcsProduct) => { const activeInternetProducts = existing.filter((product: WhmcsProduct) => {

View File

@ -52,9 +52,9 @@ export class WorkflowCaseManager {
: null; : null;
const opportunityStatus = opportunityId const opportunityStatus = opportunityId
? (opportunityCreated ? opportunityCreated
? "Created new opportunity for this order" ? "Created new opportunity for this order"
: "Linked to existing opportunity") : "Linked to existing opportunity"
: "No opportunity linked"; : "No opportunity linked";
const description = this.buildDescription([ const description = this.buildDescription([

View File

@ -77,9 +77,9 @@ export class InternetCancellationService {
const productContainer = productsResponse.products?.product; const productContainer = productsResponse.products?.product;
const products = Array.isArray(productContainer) const products = Array.isArray(productContainer)
? productContainer ? productContainer
: (productContainer : productContainer
? [productContainer] ? [productContainer]
: []); : [];
const subscription = products.find( const subscription = products.find(
(p: { id?: number | string }) => Number(p.id) === subscriptionId (p: { id?: number | string }) => Number(p.id) === subscriptionId

View File

@ -107,9 +107,9 @@ export class SimValidationService {
// Account extraction result // Account extraction result
extractedAccount, extractedAccount,
accountSource: extractedAccount accountSource: extractedAccount
? (subscription.domain ? subscription.domain
? "domain field" ? "domain field"
: "custom field or order number") : "custom field or order number"
: "NOT FOUND - check fields below", : "NOT FOUND - check fields below",
// All custom fields for debugging // All custom fields for debugging
customFieldKeys: Object.keys(subscription.customFields || {}), customFieldKeys: Object.keys(subscription.customFields || {}),

View File

@ -93,9 +93,9 @@ export class ResidenceCardService {
const reviewerNotes = const reviewerNotes =
typeof rejectionRaw === "string" && rejectionRaw.trim().length > 0 typeof rejectionRaw === "string" && rejectionRaw.trim().length > 0
? rejectionRaw.trim() ? rejectionRaw.trim()
: (typeof noteRaw === "string" && noteRaw.trim().length > 0 : typeof noteRaw === "string" && noteRaw.trim().length > 0
? noteRaw.trim() ? noteRaw.trim()
: null); : null;
return residenceCardVerificationSchema.parse({ return residenceCardVerificationSchema.parse({
status, status,

View File

@ -34,8 +34,11 @@ const nextConfig = {
config.watchOptions = { config.watchOptions = {
...config.watchOptions, ...config.watchOptions,
ignored: config.watchOptions?.ignored 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/**"], : ["**/node_modules/**"],
}; };
// Add domain dist to snapshot managed paths for better change detection // Add domain dist to snapshot managed paths for better change detection

View File

@ -149,9 +149,9 @@ export function OtpInput({
"disabled:opacity-50 disabled:cursor-not-allowed", "disabled:opacity-50 disabled:cursor-not-allowed",
error error
? "border-danger focus:ring-danger focus:border-danger" ? "border-danger focus:ring-danger focus:border-danger"
: (activeIndex === index : activeIndex === index
? "border-primary" ? "border-primary"
: "border-border hover:border-muted-foreground/50") : "border-border hover:border-muted-foreground/50"
)} )}
aria-label={`Digit ${index + 1}`} aria-label={`Digit ${index + 1}`}
/> />

View File

@ -216,9 +216,9 @@ export function AppShell({ children }: AppShellProps) {
</div> </div>
</div> </div>
</div> </div>
) : (isAuthReady ? ( ) : isAuthReady ? (
children children
) : null)} ) : null}
</main> </main>
</div> </div>
</div> </div>

View File

@ -126,11 +126,11 @@ export function AddressCard({
</AlertBanner> </AlertBanner>
)} )}
</div> </div>
) : (hasAddress ? ( ) : hasAddress ? (
<AddressDisplay address={address} /> <AddressDisplay address={address} />
) : ( ) : (
<EmptyAddressState onEdit={onEdit} /> <EmptyAddressState onEdit={onEdit} />
))} )}
</div> </div>
</div> </div>
); );

View File

@ -119,9 +119,9 @@ function fromLegacyFormat(address: LegacyAddressData): PartialJapanAddressFormDa
// For new users, leave it undefined so they must explicitly choose // For new users, leave it undefined so they must explicitly choose
const hasExistingAddress = address.postcode || address.state || address.city; const hasExistingAddress = address.postcode || address.state || address.city;
const residenceType = hasExistingAddress const residenceType = hasExistingAddress
? (roomNumber ? roomNumber
? RESIDENCE_TYPE.APARTMENT ? RESIDENCE_TYPE.APARTMENT
: RESIDENCE_TYPE.HOUSE) : RESIDENCE_TYPE.HOUSE
: undefined; : undefined;
return { return {

View File

@ -336,9 +336,9 @@ export function JapanAddressForm({
required required
helperText={ helperText={
form.address.streetAddress.trim() form.address.streetAddress.trim()
? (streetAddressError ? streetAddressError
? undefined ? undefined
: "Valid format") : "Valid format"
: "Enter chome-banchi-go (e.g., 1-5-3)" : "Enter chome-banchi-go (e.g., 1-5-3)"
} }
> >

View File

@ -23,9 +23,9 @@ export function ProgressIndicator({ currentStep, totalSteps }: ProgressIndicator
"h-1 rounded-full transition-all duration-500", "h-1 rounded-full transition-all duration-500",
i < currentStep i < currentStep
? "bg-primary flex-[2]" ? "bg-primary flex-[2]"
: (i === currentStep : i === currentStep
? "bg-primary/40 flex-[2] animate-pulse" ? "bg-primary/40 flex-[2] animate-pulse"
: "bg-border flex-1") : "bg-border flex-1"
)} )}
/> />
))} ))}

View File

@ -11,6 +11,7 @@ import {
authResponseSchema, authResponseSchema,
checkPasswordNeededResponseSchema, checkPasswordNeededResponseSchema,
loginOtpRequiredResponseSchema, loginOtpRequiredResponseSchema,
loginResponseSchema,
type AuthSession, type AuthSession,
type CheckPasswordNeededResponse, type CheckPasswordNeededResponse,
type LoginRequest, type LoginRequest,
@ -40,8 +41,10 @@ export interface AuthState {
error: string | null; error: string | null;
hasCheckedAuth: boolean; hasCheckedAuth: boolean;
// Two-step login with OTP // Two-step login with OTP (or direct login in dev mode with SKIP_OTP)
initiateLogin: (credentials: LoginRequest) => Promise<LoginOtpRequiredResponse>; initiateLogin: (
credentials: LoginRequest
) => Promise<LoginOtpRequiredResponse | { requiresOtp: false }>;
verifyLoginOtp: (sessionToken: string, code: string) => Promise<void>; verifyLoginOtp: (sessionToken: string, code: string) => Promise<void>;
// Legacy login (kept for backward compatibility during migration) // Legacy login (kept for backward compatibility during migration)
login: (credentials: LoginRequest) => Promise<void>; login: (credentials: LoginRequest) => Promise<void>;
@ -195,6 +198,7 @@ export const useAuthStore = create<AuthState>()((set, get) => {
/** /**
* Step 1 of two-step login: Validate credentials and initiate OTP * Step 1 of two-step login: Validate credentials and initiate OTP
* Returns OTP session info for the frontend to display OTP input * 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 => { initiateLogin: async credentials => {
set({ loading: true, error: null }); set({ loading: true, error: null });
@ -203,12 +207,20 @@ export const useAuthStore = create<AuthState>()((set, get) => {
body: credentials, body: credentials,
disableCsrf: true, // Public auth endpoint, exempt from CSRF disableCsrf: true, // Public auth endpoint, exempt from CSRF
}); });
const parsed = loginOtpRequiredResponseSchema.safeParse(response.data); const parsed = loginResponseSchema.safeParse(response.data);
if (!parsed.success) { if (!parsed.success) {
throw new Error(parsed.error.issues?.[0]?.message ?? "Login failed"); throw new Error(parsed.error.issues?.[0]?.message ?? "Login failed");
} }
// 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 }); set({ loading: false });
return parsed.data; return parsed.data;
}
// Direct login - apply auth response and signal completion
applyAuthResponse(parsed.data as AuthResponseData);
return { requiresOtp: false as const };
} catch (error) { } catch (error) {
const parsed = parseError(error); const parsed = parseError(error);
set({ loading: false, error: parsed.message, isAuthenticated: false }); set({ loading: false, error: parsed.message, isAuthenticated: false });

View File

@ -144,7 +144,7 @@ export function PaymentMethodsContainer() {
</div> </div>
</div> </div>
</div> </div>
) : (paymentMethodsData && paymentMethodsData.paymentMethods.length > 0 ? ( ) : paymentMethodsData && paymentMethodsData.paymentMethods.length > 0 ? (
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden"> <div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 px-6 py-5 border-b border-gray-200"> <div className="bg-gradient-to-r from-blue-50 to-indigo-50 px-6 py-5 border-b border-gray-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -214,7 +214,7 @@ export function PaymentMethodsContainer() {
</div> </div>
)} )}
</div> </div>
))} )}
</div> </div>
<div className="lg:col-span-1 xl:col-span-1"> <div className="lg:col-span-1 xl:col-span-1">

View File

@ -132,11 +132,11 @@ export function CheckoutStatusBanners({
</p> </p>
{eligibility.notes ? ( {eligibility.notes ? (
<p className="text-xs text-muted-foreground">{eligibility.notes}</p> <p className="text-xs text-muted-foreground">{eligibility.notes}</p>
) : (eligibility.requestedAt ? ( ) : eligibility.requestedAt ? (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Last updated: {new Date(eligibility.requestedAt).toLocaleString()} Last updated: {new Date(eligibility.requestedAt).toLocaleString()}
</p> </p>
) : null)} ) : null}
<Button as="a" href="/account/support/new" size="sm"> <Button as="a" href="/account/support/new" size="sm">
Contact support Contact support
</Button> </Button>

View File

@ -254,11 +254,11 @@ function NotSubmittedContent({
<div className="font-medium text-foreground">Rejection note</div> <div className="font-medium text-foreground">Rejection note</div>
<div>{reviewerNotes}</div> <div>{reviewerNotes}</div>
</div> </div>
) : (isRejected ? ( ) : isRejected ? (
<p className="text-sm text-foreground/80"> <p className="text-sm text-foreground/80">
Your document couldn't be approved. Please upload a new file to continue. Your document couldn't be approved. Please upload a new file to continue.
</p> </p>
) : null)} ) : null}
<p className="text-sm text-foreground/80"> <p className="text-sm text-foreground/80">
Upload a JPG, PNG, or PDF (max 5MB). We'll verify it before activating SIM service. Upload a JPG, PNG, or PDF (max 5MB). We'll verify it before activating SIM service.

View File

@ -1341,14 +1341,14 @@ export function PublicLandingView() {
<Spinner size="sm" /> <Spinner size="sm" />
Sending... Sending...
</> </>
) : (submitStatus === "success" ? ( ) : submitStatus === "success" ? (
<> <>
<CheckCircle className="h-4 w-4" /> <CheckCircle className="h-4 w-4" />
Sent! Sent!
</> </>
) : ( ) : (
"Submit" "Submit"
))} )}
</button> </button>
</form> </form>

View File

@ -67,7 +67,7 @@ export const NotificationDropdown = memo(function NotificationDropdown({
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" /> <div className="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
</div> </div>
) : (notifications.length === 0 ? ( ) : notifications.length === 0 ? (
<div className="flex flex-col items-center justify-center py-10 px-4 text-center"> <div className="flex flex-col items-center justify-center py-10 px-4 text-center">
<BellSlashIcon className="h-10 w-10 text-muted-foreground/40 mb-3" /> <BellSlashIcon className="h-10 w-10 text-muted-foreground/40 mb-3" />
<p className="text-sm text-muted-foreground">No notifications yet</p> <p className="text-sm text-muted-foreground">No notifications yet</p>
@ -86,7 +86,7 @@ export const NotificationDropdown = memo(function NotificationDropdown({
/> />
))} ))}
</div> </div>
))} )}
</div> </div>
{/* Footer */} {/* Footer */}

View File

@ -287,9 +287,9 @@ export function AddressForm({
const containerClasses = const containerClasses =
variant === "inline" variant === "inline"
? "" ? ""
: (variant === "compact" : variant === "compact"
? "p-4 bg-gray-50 rounded-lg border border-gray-200" ? "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 // Get all validation errors
const allErrors = Object.values(form.errors).filter(Boolean) as string[]; const allErrors = Object.values(form.errors).filter(Boolean) as string[];

View File

@ -261,7 +261,7 @@ export function OrderSummary({
</Button> </Button>
) : null} ) : null}
</> </>
) : (onContinue ? ( ) : onContinue ? (
<Button <Button
size="lg" size="lg"
className="w-full mt-8 group text-lg font-bold" className="w-full mt-8 group text-lg font-bold"
@ -271,7 +271,7 @@ export function OrderSummary({
> >
{continueLabel} {continueLabel}
</Button> </Button>
) : null)} ) : null}
</div> </div>
)} )}
</div> </div>

View File

@ -160,7 +160,7 @@ export function ProductCard({
> >
{actionLabel} {actionLabel}
</Button> </Button>
) : (onClick ? ( ) : onClick ? (
<Button <Button
onClick={onClick} onClick={onClick}
className="w-full group" className="w-full group"
@ -169,7 +169,7 @@ export function ProductCard({
> >
{actionLabel} {actionLabel}
</Button> </Button>
) : null)} ) : null}
</div> </div>
{/* Custom footer */} {/* Custom footer */}

View File

@ -37,9 +37,9 @@ export function InstallationOptions({
installation.description || installation.description ||
(installationTerm === "12-Month" (installationTerm === "12-Month"
? "Spread the installation fee across 12 monthly payments." ? "Spread the installation fee across 12 monthly payments."
: (installationTerm === "24-Month" : installationTerm === "24-Month"
? "Spread the installation fee across 24 monthly payments." ? "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 ( return (
<button <button

View File

@ -167,7 +167,7 @@ export function InternetOfferingCard({
See pricing after verification See pricing after verification
</p> </p>
</div> </div>
) : (disabled ? ( ) : disabled ? (
<div className="mt-auto"> <div className="mt-auto">
<Button variant="outline" size="sm" className="w-full" disabled> <Button variant="outline" size="sm" className="w-full" disabled>
Unavailable Unavailable
@ -188,7 +188,7 @@ export function InternetOfferingCard({
> >
Select Select
</Button> </Button>
))} )}
</div> </div>
))} ))}
</div> </div>

View File

@ -255,7 +255,7 @@ export function DeviceCompatibility() {
</p> </p>
)} )}
</div> </div>
) : (showNoResults ? ( ) : showNoResults ? (
<div className="p-6 text-center"> <div className="p-6 text-center">
<div className="flex-shrink-0 h-12 w-12 mx-auto rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center mb-3"> <div className="flex-shrink-0 h-12 w-12 mx-auto rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center mb-3">
<X className="h-6 w-6 text-amber-600 dark:text-amber-400" /> <X className="h-6 w-6 text-amber-600 dark:text-amber-400" />
@ -269,7 +269,7 @@ export function DeviceCompatibility() {
to verify compatibility. to verify compatibility.
</p> </p>
</div> </div>
) : null)} ) : null}
</div> </div>
)} )}
</div> </div>

View File

@ -266,9 +266,9 @@ export function SimPlansContent({
const tabPlans = const tabPlans =
activeTab === "data-voice" activeTab === "data-voice"
? plansByType.DataSmsVoice ? plansByType.DataSmsVoice
: (activeTab === "data-only" : activeTab === "data-only"
? plansByType.DataOnly ? plansByType.DataOnly
: plansByType.VoiceOnly); : plansByType.VoiceOnly;
const regularPlans = tabPlans.filter(p => !p.simHasFamilyDiscount); const regularPlans = tabPlans.filter(p => !p.simHasFamilyDiscount);
const familyPlans = tabPlans.filter(p => p.simHasFamilyDiscount); const familyPlans = tabPlans.filter(p => p.simHasFamilyDiscount);

View File

@ -64,9 +64,9 @@ export function useInternetConfigureParams() {
.split(",") .split(",")
.map(s => s.trim()) .map(s => s.trim())
.filter(Boolean) .filter(Boolean)
: (addonSkuParams.length > 0 : addonSkuParams.length > 0
? addonSkuParams ? addonSkuParams
: []); : [];
return { return {
accessMode, accessMode,

View File

@ -204,7 +204,7 @@ export function InternetEligibilityRequestView() {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{planLoading ? ( {planLoading ? (
<div className="text-sm text-muted-foreground">Loading selected plan</div> <div className="text-sm text-muted-foreground">Loading selected plan</div>
) : (plan ? ( ) : plan ? (
<div className="flex items-center justify-between gap-3 flex-wrap"> <div className="flex items-center justify-between gap-3 flex-wrap">
<div> <div>
<p className="text-xs text-muted-foreground">Selected plan</p> <p className="text-xs text-muted-foreground">Selected plan</p>
@ -212,7 +212,7 @@ export function InternetEligibilityRequestView() {
</div> </div>
<CardPricing monthlyPrice={plan.monthlyPrice} size="sm" alignment="right" /> <CardPricing monthlyPrice={plan.monthlyPrice} size="sm" alignment="right" />
</div> </div>
) : null)} ) : null}
</div> </div>
</div> </div>
</div> </div>

View File

@ -68,9 +68,9 @@ export function PublicEligibilityCheckView() {
step === "success" ? (hasAccount ? "Account Created" : "Request Submitted") : currentMeta.title; step === "success" ? (hasAccount ? "Account Created" : "Request Submitted") : currentMeta.title;
const description = const description =
step === "success" step === "success"
? (hasAccount ? hasAccount
? "Your account is ready and eligibility check is in progress." ? "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; : currentMeta.description;
return ( return (

View File

@ -977,7 +977,7 @@ export function PublicInternetPlansContent({
<section className="space-y-3"> <section className="space-y-3">
{isLoading ? ( {isLoading ? (
<Skeleton className="h-64 w-full rounded-xl" /> <Skeleton className="h-64 w-full rounded-xl" />
) : (consolidatedPlanData ? ( ) : consolidatedPlanData ? (
<ConsolidatedInternetCard <ConsolidatedInternetCard
minPrice={consolidatedPlanData.minPrice} minPrice={consolidatedPlanData.minPrice}
maxPrice={consolidatedPlanData.maxPrice} maxPrice={consolidatedPlanData.maxPrice}
@ -987,7 +987,7 @@ export function PublicInternetPlansContent({
ctaLabel={ctaLabel} ctaLabel={ctaLabel}
{...(onCtaClick && { onCtaClick })} {...(onCtaClick && { onCtaClick })}
/> />
) : null)} ) : null}
</section> </section>
{/* Available Plans - Expandable cards by offering type */} {/* Available Plans - Expandable cards by offering type */}

View File

@ -79,9 +79,9 @@ export function ReissueSimModal({
} catch (error: unknown) { } catch (error: unknown) {
const message = const message =
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? (error instanceof Error ? error instanceof Error
? error.message ? error.message
: "Failed to submit reissue request") : "Failed to submit reissue request"
: "Failed to submit reissue request. Please try again."; : "Failed to submit reissue request. Please try again.";
onError(message); onError(message);
} finally { } finally {

View File

@ -388,9 +388,9 @@ function useSimActionsState(subscriptionId: number, onCancelSuccess?: () => void
} catch (err: unknown) { } catch (err: unknown) {
setError( setError(
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? (err instanceof Error ? err instanceof Error
? err.message ? err.message
: "Failed to cancel SIM service") : "Failed to cancel SIM service"
: "Unable to cancel SIM service right now. Please try again." : "Unable to cancel SIM service right now. Please try again."
); );
} finally { } finally {

View File

@ -108,9 +108,9 @@ export function SimManagementSection({ subscriptionId }: SimManagementSectionPro
} else { } else {
setError( setError(
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? (err instanceof Error ? err instanceof Error
? err.message ? err.message
: "Failed to load SIM information") : "Failed to load SIM information"
: "Unable to load SIM information right now. Please try again." : "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 remainingMB = simInfo.details.remainingQuotaMb.toFixed(1);
const usedMB = simInfo.usage?.monthlyUsageMb const usedMB = simInfo.usage?.monthlyUsageMb
? simInfo.usage.monthlyUsageMb.toFixed(2) ? simInfo.usage.monthlyUsageMb.toFixed(2)
: (simInfo.usage?.todayUsageMb : simInfo.usage?.todayUsageMb
? simInfo.usage.todayUsageMb.toFixed(2) ? simInfo.usage.todayUsageMb.toFixed(2)
: "0.00"); : "0.00";
// Calculate percentage for circle // Calculate percentage for circle
const totalMB = Number.parseFloat(remainingMB) + Number.parseFloat(usedMB); const totalMB = Number.parseFloat(remainingMB) + Number.parseFloat(usedMB);

View File

@ -140,9 +140,9 @@ export function CancelSubscriptionContainer() {
} catch (e: unknown) { } catch (e: unknown) {
setError( setError(
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? (e instanceof Error ? e instanceof Error
? e.message ? e.message
: "Failed to load cancellation information") : "Failed to load cancellation information"
: "Unable to load cancellation information right now. Please try again." : "Unable to load cancellation information right now. Please try again."
); );
} finally { } finally {
@ -176,9 +176,9 @@ export function CancelSubscriptionContainer() {
} catch (e: unknown) { } catch (e: unknown) {
setFormError( setFormError(
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? (e instanceof Error ? e instanceof Error
? e.message ? e.message
: "Failed to submit cancellation") : "Failed to submit cancellation"
: "Unable to submit your cancellation right now. Please try again." : "Unable to submit your cancellation right now. Please try again."
); );
} finally { } finally {

View File

@ -32,9 +32,9 @@ export function SimChangePlanContainer() {
} catch (e: unknown) { } catch (e: unknown) {
setError( setError(
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? (e instanceof Error ? e instanceof Error
? e.message ? e.message
: "Failed to load available plans") : "Failed to load available plans"
: "Unable to load available plans right now. Please try again." : "Unable to load available plans right now. Please try again."
); );
} finally { } finally {
@ -66,9 +66,9 @@ export function SimChangePlanContainer() {
} catch (e: unknown) { } catch (e: unknown) {
setError( setError(
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? (e instanceof Error ? e instanceof Error
? e.message ? e.message
: "Failed to change plan") : "Failed to change plan"
: "Unable to submit your plan change right now. Please try again." : "Unable to submit your plan change right now. Please try again."
); );
} finally { } finally {

View File

@ -37,9 +37,9 @@ export function SimReissueContainer() {
} catch (e: unknown) { } catch (e: unknown) {
setError( setError(
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? (e instanceof Error ? e instanceof Error
? e.message ? e.message
: "Failed to load SIM details") : "Failed to load SIM details"
: "Unable to load SIM details right now. Please try again." : "Unable to load SIM details right now. Please try again."
); );
} finally { } finally {
@ -85,9 +85,9 @@ export function SimReissueContainer() {
} catch (e: unknown) { } catch (e: unknown) {
setError( setError(
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? (e instanceof Error ? e instanceof Error
? e.message ? e.message
: "Failed to submit reissue request") : "Failed to submit reissue request"
: "Unable to submit your request right now. Please try again." : "Unable to submit your request right now. Please try again."
); );
} finally { } finally {

View File

@ -54,9 +54,9 @@ export function SimTopUpContainer() {
} catch (e: unknown) { } catch (e: unknown) {
setError( setError(
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? (e instanceof Error ? e instanceof Error
? e.message ? e.message
: "Failed to submit top-up") : "Failed to submit top-up"
: "Unable to submit your top-up right now. Please try again." : "Unable to submit your top-up right now. Please try again."
); );
} finally { } finally {

View File

@ -57,9 +57,9 @@ export function SubscriptionDetailContainer() {
// Show error message (only when we have an error, not during loading) // Show error message (only when we have an error, not during loading)
const pageError = error const pageError = error
? (process.env.NODE_ENV === "development" && error instanceof Error ? process.env.NODE_ENV === "development" && error instanceof Error
? error.message ? error.message
: "Unable to load subscription details. Please try again.") : "Unable to load subscription details. Please try again."
: null; : null;
const productNameLower = subscription?.productName?.toLowerCase() ?? ""; const productNameLower = subscription?.productName?.toLowerCase() ?? "";

View File

@ -42,9 +42,9 @@ export function NewSupportCaseView() {
} catch (err) { } catch (err) {
setError( setError(
process.env.NODE_ENV === "development" process.env.NODE_ENV === "development"
? (err instanceof Error ? err instanceof Error
? err.message ? err.message
: "Failed to create support case") : "Failed to create support case"
: "Unable to create your support case right now. Please try again." : "Unable to create your support case right now. Please try again."
); );
} }

View File

@ -212,7 +212,7 @@ export function SupportCasesView() {
</div> </div>
))} ))}
</div> </div>
) : (hasActiveFilters ? ( ) : hasActiveFilters ? (
<AnimatedCard className="p-8" variant="static"> <AnimatedCard className="p-8" variant="static">
<SearchEmptyState searchTerm={searchTerm || "filters"} onClearSearch={clearFilters} /> <SearchEmptyState searchTerm={searchTerm || "filters"} onClearSearch={clearFilters} />
</AnimatedCard> </AnimatedCard>
@ -228,7 +228,7 @@ export function SupportCasesView() {
}} }}
/> />
</AnimatedCard> </AnimatedCard>
))} )}
</PageLayout> </PageLayout>
); );
} }

View File

@ -29,6 +29,9 @@ DISABLE_CSRF=true
DISABLE_RATE_LIMIT=true DISABLE_RATE_LIMIT=true
DISABLE_ACCOUNT_LOCKING=true DISABLE_ACCOUNT_LOCKING=true
# Skip OTP verification during login (direct login after credentials)
SKIP_OTP=true
# Show detailed validation errors in responses # Show detailed validation errors in responses
EXPOSE_VALIDATION_ERRORS=true EXPOSE_VALIDATION_ERRORS=true

View File

@ -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<string, unknown> {
const config: Record<string, unknown> = {};
if (rawConfigurations && typeof rawConfigurations === "object") {
Object.assign(config, rawConfigurations as Record<string, unknown>);
}
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<string, unknown>,
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<string, unknown>,
idempotencyKey: string
): Promise<OrderFulfillmentContext> {
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<string, unknown>
): 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<void> {
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<void> {
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<T>(
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<AccountStatus> {
// 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

1
env/dev.env.sample vendored
View File

@ -106,3 +106,4 @@ SENDGRID_API_KEY=
# DISABLE_CSRF=false # DISABLE_CSRF=false
# DISABLE_RATE_LIMIT=false # DISABLE_RATE_LIMIT=false
# DISABLE_ACCOUNT_LOCKING=false # DISABLE_ACCOUNT_LOCKING=false
# SKIP_OTP=false # Skip OTP verification during login (dev only)

View File

@ -585,7 +585,7 @@ export function getOrderTrackingSteps(
return stages.map((s, index) => ({ return stages.map((s, index) => ({
label: s.label, label: s.label,
status: index < currentStep ? "completed" : (index === currentStep ? "current" : "upcoming"), status: index < currentStep ? "completed" : index === currentStep ? "current" : "upcoming",
})); }));
} }

View File

@ -195,9 +195,9 @@ export function transformWhmcsSubscriptionListResponse(
const productContainer = parsed.products?.product; const productContainer = parsed.products?.product;
const products = Array.isArray(productContainer) const products = Array.isArray(productContainer)
? productContainer ? productContainer
: (productContainer : productContainer
? [productContainer] ? [productContainer]
: []); : [];
const subscriptions: Subscription[] = []; const subscriptions: Subscription[] = [];
for (const product of products) { for (const product of products) {
@ -213,9 +213,9 @@ export function transformWhmcsSubscriptionListResponse(
const totalResults = const totalResults =
typeof totalResultsRaw === "number" typeof totalResultsRaw === "number"
? totalResultsRaw ? totalResultsRaw
: (typeof totalResultsRaw === "string" : typeof totalResultsRaw === "string"
? Number.parseInt(totalResultsRaw, 10) ? Number.parseInt(totalResultsRaw, 10)
: subscriptions.length); : subscriptions.length;
if (status) { if (status) {
const normalizedStatus = subscriptionStatusSchema.parse(status); const normalizedStatus = subscriptionStatusSchema.parse(status);