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:
parent
2dec0af63b
commit
7abd433d95
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
|
@ -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",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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([
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 || {}),
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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");
|
||||||
}
|
}
|
||||||
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) {
|
} 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 });
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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[];
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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() ?? "";
|
||||||
|
|||||||
@ -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."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
1
env/dev.env.sample
vendored
@ -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)
|
||||||
|
|||||||
@ -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",
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user