Revamp Physical SIM activation to use PA02-01 + PA05-05
Replace PA05-33 OTA API with the proper two-step activation flow: - PA02-01: Account Registration (/master/addAcnt/) - PA05-05: Voice Options Registration (/mvno/talkoption/addOrder/) Changes: - Add FreebitAccountRegistrationService for PA02-01 account registration - Add FreebitVoiceOptionsService for PA05-05 voice options - Update SimFulfillmentService to use new APIs instead of PA05-33 OTA - Add SalesforceSIMInventoryService for fetching SIM inventory data - Remove deprecated FreebitOtaService (PA05-33 no longer used) - Remove debug console.log statements The new flow: 1. Fetch SIM inventory from Salesforce (phone number, PT number) 2. Call PA02-01 to register MVNO account with plan code 3. Call PA05-05 to configure voice options with customer identity 4. Update SIM inventory status to "In Use" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
56189a9fe8
commit
1283880f7d
@ -1,50 +1,50 @@
|
|||||||
1,02000002470001,PT0220024700010,PASI,20251229,,,,
|
1,02000002470001,PT0220024700010,PASI,2025-12-29,,,,
|
||||||
2,02000002470002,PT0220024700020,PASI,20251229,,,,
|
2,02000002470002,PT0220024700020,PASI,2025-12-29,,,,
|
||||||
3,02000002470003,PT0220024700030,PASI,20251229,,,,
|
3,02000002470003,PT0220024700030,PASI,2025-12-29,,,,
|
||||||
4,02000002470004,PT0220024700040,PASI,20251229,,,,
|
4,02000002470004,PT0220024700040,PASI,2025-12-29,,,,
|
||||||
5,02000002470005,PT0220024700050,PASI,20251229,,,,
|
5,02000002470005,PT0220024700050,PASI,2025-12-29,,,,
|
||||||
6,02000002470006,PT0220024700060,PASI,20251229,,,,
|
6,02000002470006,PT0220024700060,PASI,2025-12-29,,,,
|
||||||
7,02000002470007,PT0220024700070,PASI,20251229,,,,
|
7,02000002470007,PT0220024700070,PASI,2025-12-29,,,,
|
||||||
8,02000002470008,PT0220024700080,PASI,20251229,,,,
|
8,02000002470008,PT0220024700080,PASI,2025-12-29,,,,
|
||||||
9,02000002470009,PT0220024700090,PASI,20251229,,,,
|
9,02000002470009,PT0220024700090,PASI,2025-12-29,,,,
|
||||||
10,02000002470010,PT0220024700100,PASI,20251229,,,,
|
10,02000002470010,PT0220024700100,PASI,2025-12-29,,,,
|
||||||
11,02000002470011,PT0220024700110,PASI,20251229,,,,
|
11,02000002470011,PT0220024700110,PASI,2025-12-29,,,,
|
||||||
12,02000002470012,PT0220024700120,PASI,20251229,,,,
|
12,02000002470012,PT0220024700120,PASI,2025-12-29,,,,
|
||||||
13,02000002470013,PT0220024700130,PASI,20251229,,,,
|
13,02000002470013,PT0220024700130,PASI,2025-12-29,,,,
|
||||||
14,02000002470014,PT0220024700140,PASI,20251229,,,,
|
14,02000002470014,PT0220024700140,PASI,2025-12-29,,,,
|
||||||
15,02000002470015,PT0220024700150,PASI,20251229,,,,
|
15,02000002470015,PT0220024700150,PASI,2025-12-29,,,,
|
||||||
16,02000002470016,PT0220024700160,PASI,20251229,,,,
|
16,02000002470016,PT0220024700160,PASI,2025-12-29,,,,
|
||||||
17,02000002470017,PT0220024700170,PASI,20251229,,,,
|
17,02000002470017,PT0220024700170,PASI,2025-12-29,,,,
|
||||||
18,02000002470018,PT0220024700180,PASI,20251229,,,,
|
18,02000002470018,PT0220024700180,PASI,2025-12-29,,,,
|
||||||
19,02000002470019,PT0220024700190,PASI,20251229,,,,
|
19,02000002470019,PT0220024700190,PASI,2025-12-29,,,,
|
||||||
20,02000002470020,PT0220024700200,PASI,20251229,,,,
|
20,02000002470020,PT0220024700200,PASI,2025-12-29,,,,
|
||||||
21,02000002470021,PT0220024700210,PASI,20251229,,,,
|
21,02000002470021,PT0220024700210,PASI,2025-12-29,,,,
|
||||||
22,02000002470022,PT0220024700220,PASI,20251229,,,,
|
22,02000002470022,PT0220024700220,PASI,2025-12-29,,,,
|
||||||
23,02000002470023,PT0220024700230,PASI,20251229,,,,
|
23,02000002470023,PT0220024700230,PASI,2025-12-29,,,,
|
||||||
24,02000002470024,PT0220024700240,PASI,20251229,,,,
|
24,02000002470024,PT0220024700240,PASI,2025-12-29,,,,
|
||||||
25,02000002470025,PT0220024700250,PASI,20251229,,,,
|
25,02000002470025,PT0220024700250,PASI,2025-12-29,,,,
|
||||||
26,02000002470026,PT0220024700260,PASI,20251229,,,,
|
26,02000002470026,PT0220024700260,PASI,2025-12-29,,,,
|
||||||
27,02000002470027,PT0220024700270,PASI,20251229,,,,
|
27,02000002470027,PT0220024700270,PASI,2025-12-29,,,,
|
||||||
28,02000002470028,PT0220024700280,PASI,20251229,,,,
|
28,02000002470028,PT0220024700280,PASI,2025-12-29,,,,
|
||||||
29,02000002470029,PT0220024700290,PASI,20251229,,,,
|
29,02000002470029,PT0220024700290,PASI,2025-12-29,,,,
|
||||||
30,02000002470030,PT0220024700300,PASI,20251229,,,,
|
30,02000002470030,PT0220024700300,PASI,2025-12-29,,,,
|
||||||
31,02000002470031,PT0220024700310,PASI,20251229,,,,
|
31,02000002470031,PT0220024700310,PASI,2025-12-29,,,,
|
||||||
32,02000002470032,PT0220024700320,PASI,20251229,,,,
|
32,02000002470032,PT0220024700320,PASI,2025-12-29,,,,
|
||||||
33,02000002470033,PT0220024700330,PASI,20251229,,,,
|
33,02000002470033,PT0220024700330,PASI,2025-12-29,,,,
|
||||||
34,02000002470034,PT0220024700340,PASI,20251229,,,,
|
34,02000002470034,PT0220024700340,PASI,2025-12-29,,,,
|
||||||
35,02000002470035,PT0220024700350,PASI,20251229,,,,
|
35,02000002470035,PT0220024700350,PASI,2025-12-29,,,,
|
||||||
36,02000002470036,PT0220024700360,PASI,20251229,,,,
|
36,02000002470036,PT0220024700360,PASI,2025-12-29,,,,
|
||||||
37,02000002470037,PT0220024700370,PASI,20251229,,,,
|
37,02000002470037,PT0220024700370,PASI,2025-12-29,,,,
|
||||||
38,02000002470038,PT0220024700380,PASI,20251229,,,,
|
38,02000002470038,PT0220024700380,PASI,2025-12-29,,,,
|
||||||
39,02000002470039,PT0220024700390,PASI,20251229,,,,
|
39,02000002470039,PT0220024700390,PASI,2025-12-29,,,,
|
||||||
40,02000002470040,PT0220024700400,PASI,20251229,,,,
|
40,02000002470040,PT0220024700400,PASI,2025-12-29,,,,
|
||||||
41,02000002470041,PT0220024700410,PASI,20251229,,,,
|
41,02000002470041,PT0220024700410,PASI,2025-12-29,,,,
|
||||||
42,02000002470042,PT0220024700420,PASI,20251229,,,,
|
42,02000002470042,PT0220024700420,PASI,2025-12-29,,,,
|
||||||
43,02000002470043,PT0220024700430,PASI,20251229,,,,
|
43,02000002470043,PT0220024700430,PASI,2025-12-29,,,,
|
||||||
44,02000002470044,PT0220024700440,PASI,20251229,,,,
|
44,02000002470044,PT0220024700440,PASI,2025-12-29,,,,
|
||||||
45,02000002470045,PT0220024700450,PASI,20251229,,,,
|
45,02000002470045,PT0220024700450,PASI,2025-12-29,,,,
|
||||||
46,02000002470046,PT0220024700460,PASI,20251229,,,,
|
46,02000002470046,PT0220024700460,PASI,2025-12-29,,,,
|
||||||
47,02000002470047,PT0220024700470,PASI,20251229,,,,
|
47,02000002470047,PT0220024700470,PASI,2025-12-29,,,,
|
||||||
48,02000002470048,PT0220024700480,PASI,20251229,,,,
|
48,02000002470048,PT0220024700480,PASI,2025-12-29,,,,
|
||||||
49,02000002470049,PT0220024700490,PASI,20251229,,,,
|
49,02000002470049,PT0220024700490,PASI,2025-12-29,,,,
|
||||||
50,02000002470050,PT0220024700500,PASI,20251229,,,,
|
50,02000002470050,PT0220024700500,PASI,2025-12-29,,,,
|
||||||
|
|||||||
|
@ -1,50 +1,50 @@
|
|||||||
51,07000240001,PT0270002400010,PASI,2025-12-29,,,,
|
51,7000240001,PT0270002400010,PASI,12/29/2025
|
||||||
52,07000240002,PT0270002400020,PASI,2025-12-29,,,,
|
52,7000240002,PT0270002400020,PASI,12/29/2025
|
||||||
53,07000240003,PT0270002400030,PASI,2025-12-29,,,,
|
53,7000240003,PT0270002400030,PASI,12/29/2025
|
||||||
54,07000240004,PT0270002400040,PASI,2025-12-29,,,,
|
54,7000240004,PT0270002400040,PASI,12/29/2025
|
||||||
55,07000240005,PT0270002400050,PASI,2025-12-29,,,,
|
55,7000240005,PT0270002400050,PASI,12/29/2025
|
||||||
56,07000240006,PT0270002400060,PASI,2025-12-29,,,,
|
56,7000240006,PT0270002400060,PASI,12/29/2025
|
||||||
57,07000240007,PT0270002400070,PASI,2025-12-29,,,,
|
57,7000240007,PT0270002400070,PASI,12/29/2025
|
||||||
58,07000240008,PT0270002400080,PASI,2025-12-29,,,,
|
58,7000240008,PT0270002400080,PASI,12/29/2025
|
||||||
59,07000240009,PT0270002400090,PASI,2025-12-29,,,,
|
59,7000240009,PT0270002400090,PASI,12/29/2025
|
||||||
60,07000240010,PT0270002400100,PASI,2025-12-29,,,,
|
60,7000240010,PT0270002400100,PASI,12/29/2025
|
||||||
61,07000240011,PT0270002400110,PASI,2025-12-29,,,,
|
61,7000240011,PT0270002400110,PASI,12/29/2025
|
||||||
62,07000240012,PT0270002400120,PASI,2025-12-29,,,,
|
62,7000240012,PT0270002400120,PASI,12/29/2025
|
||||||
63,07000240013,PT0270002400130,PASI,2025-12-29,,,,
|
63,7000240013,PT0270002400130,PASI,12/29/2025
|
||||||
64,07000240014,PT0270002400140,PASI,2025-12-29,,,,
|
64,7000240014,PT0270002400140,PASI,12/29/2025
|
||||||
65,07000240015,PT0270002400150,PASI,2025-12-29,,,,
|
65,7000240015,PT0270002400150,PASI,12/29/2025
|
||||||
66,07000240016,PT0270002400160,PASI,2025-12-29,,,,
|
66,7000240016,PT0270002400160,PASI,12/29/2025
|
||||||
67,07000240017,PT0270002400170,PASI,2025-12-29,,,,
|
67,7000240017,PT0270002400170,PASI,12/29/2025
|
||||||
68,07000240018,PT0270002400180,PASI,2025-12-29,,,,
|
68,7000240018,PT0270002400180,PASI,12/29/2025
|
||||||
69,07000240019,PT0270002400190,PASI,2025-12-29,,,,
|
69,7000240019,PT0270002400190,PASI,12/29/2025
|
||||||
70,07000240020,PT0270002400200,PASI,2025-12-29,,,,
|
70,7000240020,PT0270002400200,PASI,12/29/2025
|
||||||
71,07000240021,PT0270002400210,PASI,2025-12-29,,,,
|
71,7000240021,PT0270002400210,PASI,12/29/2025
|
||||||
72,07000240022,PT0270002400220,PASI,2025-12-29,,,,
|
72,7000240022,PT0270002400220,PASI,12/29/2025
|
||||||
73,07000240023,PT0270002400230,PASI,2025-12-29,,,,
|
73,7000240023,PT0270002400230,PASI,12/29/2025
|
||||||
74,07000240024,PT0270002400240,PASI,2025-12-29,,,,
|
74,7000240024,PT0270002400240,PASI,12/29/2025
|
||||||
75,07000240025,PT0270002400250,PASI,2025-12-29,,,,
|
75,7000240025,PT0270002400250,PASI,12/29/2025
|
||||||
76,07000240026,PT0270002400260,PASI,2025-12-29,,,,
|
76,7000240026,PT0270002400260,PASI,12/29/2025
|
||||||
77,07000240027,PT0270002400270,PASI,2025-12-29,,,,
|
77,7000240027,PT0270002400270,PASI,12/29/2025
|
||||||
78,07000240028,PT0270002400280,PASI,2025-12-29,,,,
|
78,7000240028,PT0270002400280,PASI,12/29/2025
|
||||||
79,07000240029,PT0270002400290,PASI,2025-12-29,,,,
|
79,7000240029,PT0270002400290,PASI,12/29/2025
|
||||||
80,07000240030,PT0270002400300,PASI,2025-12-29,,,,
|
80,7000240030,PT0270002400300,PASI,12/29/2025
|
||||||
81,07000240031,PT0270002400310,PASI,2025-12-29,,,,
|
81,7000240031,PT0270002400310,PASI,12/29/2025
|
||||||
82,07000240032,PT0270002400320,PASI,2025-12-29,,,,
|
82,7000240032,PT0270002400320,PASI,12/29/2025
|
||||||
83,07000240033,PT0270002400330,PASI,2025-12-29,,,,
|
83,7000240033,PT0270002400330,PASI,12/29/2025
|
||||||
84,07000240034,PT0270002400340,PASI,2025-12-29,,,,
|
84,7000240034,PT0270002400340,PASI,12/29/2025
|
||||||
85,07000240035,PT0270002400350,PASI,2025-12-29,,,,
|
85,7000240035,PT0270002400350,PASI,12/29/2025
|
||||||
86,07000240036,PT0270002400360,PASI,2025-12-29,,,,
|
86,7000240036,PT0270002400360,PASI,12/29/2025
|
||||||
87,07000240037,PT0270002400370,PASI,2025-12-29,,,,
|
87,7000240037,PT0270002400370,PASI,12/29/2025
|
||||||
88,07000240038,PT0270002400380,PASI,2025-12-29,,,,
|
88,7000240038,PT0270002400380,PASI,12/29/2025
|
||||||
89,07000240039,PT0270002400390,PASI,2025-12-29,,,,
|
89,7000240039,PT0270002400390,PASI,12/29/2025
|
||||||
90,07000240040,PT0270002400400,PASI,2025-12-29,,,,
|
90,7000240040,PT0270002400400,PASI,12/29/2025
|
||||||
91,07000240041,PT0270002400410,PASI,2025-12-29,,,,
|
91,7000240041,PT0270002400410,PASI,12/29/2025
|
||||||
92,07000240042,PT0270002400420,PASI,2025-12-29,,,,
|
92,7000240042,PT0270002400420,PASI,12/29/2025
|
||||||
93,07000240043,PT0270002400430,PASI,2025-12-29,,,,
|
93,7000240043,PT0270002400430,PASI,12/29/2025
|
||||||
94,07000240044,PT0270002400440,PASI,2025-12-29,,,,
|
94,7000240044,PT0270002400440,PASI,12/29/2025
|
||||||
95,07000240045,PT0270002400450,PASI,2025-12-29,,,,
|
95,7000240045,PT0270002400450,PASI,12/29/2025
|
||||||
96,07000240046,PT0270002400460,PASI,2025-12-29,,,,
|
96,7000240046,PT0270002400460,PASI,12/29/2025
|
||||||
97,07000240047,PT0270002400470,PASI,2025-12-29,,,,
|
97,7000240047,PT0270002400470,PASI,12/29/2025
|
||||||
98,07000240048,PT0270002400480,PASI,2025-12-29,,,,
|
98,7000240048,PT0270002400480,PASI,12/29/2025
|
||||||
99,07000240049,PT0270002400490,PASI,2025-12-29,,,,
|
99,7000240049,PT0270002400490,PASI,12/29/2025
|
||||||
100,07000240050,PT0270002400500,PASI,2025-12-29,,,,
|
100,7000240050,PT0270002400500,PASI,12/29/2025
|
||||||
|
|||||||
|
@ -6,3 +6,15 @@ Timestamp,API Endpoint,API Method,Phone Number,SIM Identifier,Request Payload,Re
|
|||||||
2026-01-29T05:54:55.030Z,/mvno/getTrafficInfo/,POST,07000240050,07000240050,"{""account"":""07000240050""}",Error: 210,API Error: NG,API Error: NG
|
2026-01-29T05:54:55.030Z,/mvno/getTrafficInfo/,POST,07000240050,07000240050,"{""account"":""07000240050""}",Error: 210,API Error: NG,API Error: NG
|
||||||
2026-01-29T05:54:59.051Z,/mvno/getTrafficInfo/,POST,07000240050,07000240050,"{""account"":""07000240050""}",Error: 210,API Error: NG,API Error: NG
|
2026-01-29T05:54:59.051Z,/mvno/getTrafficInfo/,POST,07000240050,07000240050,"{""account"":""07000240050""}",Error: 210,API Error: NG,API Error: NG
|
||||||
2026-01-29T05:55:03.587Z,/mvno/getTrafficInfo/,POST,07000240050,07000240050,"{""account"":""07000240050""}",Error: 210,API Error: NG,API Error: NG
|
2026-01-29T05:55:03.587Z,/mvno/getTrafficInfo/,POST,07000240050,07000240050,"{""account"":""07000240050""}",Error: 210,API Error: NG,API Error: NG
|
||||||
|
2026-01-30T05:00:27.476Z,/mvno/getTrafficInfo/,POST,07000240050,07000240050,"{""account"":""07000240050""}",Error: 210,API Error: NG,API Error: NG
|
||||||
|
2026-01-30T05:00:30.722Z,/mvno/getTrafficInfo/,POST,07000240050,07000240050,"{""account"":""07000240050""}",Error: 210,API Error: NG,API Error: NG
|
||||||
|
2026-01-30T05:00:41.667Z,/mvno/getTrafficInfo/,POST,2877252932,2877252932,"{""account"":""2877252932""}",Error: 210,API Error: NG,API Error: NG
|
||||||
|
2026-01-30T05:00:41.940Z,/mvno/getTrafficInfo/,POST,2877252932,2877252932,"{""account"":""2877252932""}",Error: 210,API Error: NG,API Error: NG
|
||||||
|
2026-01-30T05:00:43.417Z,/mvno/getTrafficInfo/,POST,2877252932,2877252932,"{""account"":""2877252932""}",Error: 210,API Error: NG,API Error: NG
|
||||||
|
2026-01-30T05:00:46.655Z,/mvno/getTrafficInfo/,POST,2877252932,2877252932,"{""account"":""2877252932""}",Error: 210,API Error: NG,API Error: NG
|
||||||
|
2026-01-30T06:41:21.669Z,/mvno/ota/addAcnt/,POST,07000240050,07000240050,"{""authKey"":""[REDACTED]"",""aladinOperated"":""10"",""createType"":""new"",""account"":""07000240050"",""productNumber"":""PT0270002400500"",""planCode"":""PASI_5G"",""contractLine"":""5G"",""size"":""nano"",""addKind"":""N"",""deliveryCode"":""PASI""}",Error: 204,API Error: Bad Request,API Error: Bad Request
|
||||||
|
2026-01-30T06:44:46.220Z,/mvno/ota/addAcnt/,POST,07000240050,07000240050,"{""authKey"":""[REDACTED]"",""aladinOperated"":""10"",""createType"":""new"",""account"":""07000240050"",""productNumber"":""PT0270002400500"",""planCode"":""PASI_5G"",""contractLine"":""5G"",""size"":""nano"",""addKind"":""N"",""deliveryCode"":""PASI""}",Error: 204,API Error: Bad Request,API Error: Bad Request
|
||||||
|
2026-01-30T06:54:15.819Z,/mvno/ota/addAcnt/,POST,07000240050,07000240050,"{""authKey"":""[REDACTED]"",""aladinOperated"":""10"",""createType"":""new"",""account"":""07000240050"",""productNumber"":""PT0270002400500"",""simkind"":""3MS"",""planCode"":""PASI_5G"",""contractLine"":""5G"",""size"":""nano"",""addKind"":""N"",""shipDate"":""20260130"",""deliveryCode"":""PASI""}",Error: 204,API Error: Bad Request,API Error: Bad Request
|
||||||
|
2026-01-30T07:00:25.099Z,/mvno/ota/addAcnt/,POST,07000240050,07000240050,"{""authKey"":""[REDACTED]"",""aladinOperated"":""10"",""createType"":""new"",""account"":""07000240050"",""productNumber"":""PT0270002400500"",""size"":""nano"",""planCode"":""PASI_5G"",""deliveryCode"":""PASI"",""addKind"":""N"",""shipDate"":""20260130""}",Error: 204,API Error: Bad Request,API Error: Bad Request
|
||||||
|
2026-01-30T07:05:10.543Z,/mvno/ota/addAcnt/,POST,07000240050,07000240050,"{""authKey"":""[REDACTED]"",""aladinOperated"":""10"",""createType"":""new"",""account"":""07000240050"",""productNumber"":""0270002400500"",""size"":""nano"",""planCode"":""PASI_5G"",""deliveryCode"":""PASI"",""addKind"":""N"",""shipDate"":""20260130""}",Error: 204,API Error: Bad Request,API Error: Bad Request
|
||||||
|
2026-01-30T07:08:08.685Z,/mvno/ota/addAcnt/,POST,07000240050,07000240050,"{""aladinOperated"":""10"",""createType"":""new"",""account"":""07000240050"",""productNumber"":""0270002400500"",""size"":""nano"",""planCode"":""PASI_5G"",""deliveryCode"":""PASI"",""addKind"":""N"",""shipDate"":""20260130""}",Error: 201,API Error: Bad Request,API Error: Bad Request
|
||||||
|
|||||||
|
@ -12,6 +12,8 @@ import { FreebitVoiceService } from "./services/freebit-voice.service.js";
|
|||||||
import { FreebitCancellationService } from "./services/freebit-cancellation.service.js";
|
import { FreebitCancellationService } from "./services/freebit-cancellation.service.js";
|
||||||
import { FreebitEsimService } from "./services/freebit-esim.service.js";
|
import { FreebitEsimService } from "./services/freebit-esim.service.js";
|
||||||
import { FreebitTestTrackerService } from "./services/freebit-test-tracker.service.js";
|
import { FreebitTestTrackerService } from "./services/freebit-test-tracker.service.js";
|
||||||
|
import { FreebitAccountRegistrationService } from "./services/freebit-account-registration.service.js";
|
||||||
|
import { FreebitVoiceOptionsService } from "./services/freebit-voice-options.service.js";
|
||||||
import { SimManagementModule } from "../../modules/subscriptions/sim-management/sim-management.module.js";
|
import { SimManagementModule } from "../../modules/subscriptions/sim-management/sim-management.module.js";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -30,6 +32,9 @@ import { SimManagementModule } from "../../modules/subscriptions/sim-management/
|
|||||||
FreebitVoiceService,
|
FreebitVoiceService,
|
||||||
FreebitCancellationService,
|
FreebitCancellationService,
|
||||||
FreebitEsimService,
|
FreebitEsimService,
|
||||||
|
// Physical SIM activation services (PA02-01 + PA05-05)
|
||||||
|
FreebitAccountRegistrationService,
|
||||||
|
FreebitVoiceOptionsService,
|
||||||
// Facade (delegates to specialized services)
|
// Facade (delegates to specialized services)
|
||||||
FreebitOperationsService,
|
FreebitOperationsService,
|
||||||
FreebitOrchestratorService,
|
FreebitOrchestratorService,
|
||||||
@ -46,6 +51,9 @@ import { SimManagementModule } from "../../modules/subscriptions/sim-management/
|
|||||||
FreebitVoiceService,
|
FreebitVoiceService,
|
||||||
FreebitCancellationService,
|
FreebitCancellationService,
|
||||||
FreebitEsimService,
|
FreebitEsimService,
|
||||||
|
// Physical SIM activation services (PA02-01 + PA05-05)
|
||||||
|
FreebitAccountRegistrationService,
|
||||||
|
FreebitVoiceOptionsService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class FreebitModule {}
|
export class FreebitModule {}
|
||||||
|
|||||||
@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* Freebit Account Registration Service (PA02-01)
|
||||||
|
*
|
||||||
|
* Handles MVNO account registration via the Freebit PA02-01 API.
|
||||||
|
* This is the first step in Physical SIM activation - registering
|
||||||
|
* the phone number (account) in Freebit's system.
|
||||||
|
*
|
||||||
|
* @see docs/freebit-apis/PA02-01-account-registration.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Inject, BadRequestException } from "@nestjs/common";
|
||||||
|
import { Logger } from "nestjs-pino";
|
||||||
|
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
|
||||||
|
import { FreebitClientService } from "./freebit-client.service.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PA02-01 Account Registration parameters
|
||||||
|
*/
|
||||||
|
export interface AccountRegistrationParams {
|
||||||
|
/** MSISDN (phone number) - 11-14 digits */
|
||||||
|
account: string;
|
||||||
|
/** Freebit plan code (e.g., "PASI_5G") */
|
||||||
|
planCode: string;
|
||||||
|
/** Global IP assignment: "10" = valid (deprecated), "20" = invalid */
|
||||||
|
globalIp?: "10" | "20";
|
||||||
|
/** Priority flag: "10" = valid, "20" = invalid (default) */
|
||||||
|
priorityFlag?: "10" | "20";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PA02-01 Request payload structure
|
||||||
|
*/
|
||||||
|
interface FreebitAccountRegistrationRequest {
|
||||||
|
createType: "new" | "add";
|
||||||
|
requestDatas: Array<{
|
||||||
|
kind: "MVNO";
|
||||||
|
account: string;
|
||||||
|
planCode: string;
|
||||||
|
globalIp?: string;
|
||||||
|
priorityFlag?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PA02-01 Response structure
|
||||||
|
*/
|
||||||
|
interface FreebitAccountRegistrationResponse {
|
||||||
|
resultCode: string;
|
||||||
|
status?: {
|
||||||
|
message?: string;
|
||||||
|
statusCode?: string | number;
|
||||||
|
};
|
||||||
|
responseDatas?: Array<{
|
||||||
|
kind: string;
|
||||||
|
account: string;
|
||||||
|
ipv4?: string;
|
||||||
|
ipv6?: string;
|
||||||
|
resultCode: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FreebitAccountRegistrationService {
|
||||||
|
constructor(
|
||||||
|
private readonly client: FreebitClientService,
|
||||||
|
@Inject(Logger) private readonly logger: Logger
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new MVNO account (PA02-01)
|
||||||
|
*
|
||||||
|
* This creates a new account in Freebit's system with the specified
|
||||||
|
* phone number and plan. This must be called before PA05-05 voice options.
|
||||||
|
*
|
||||||
|
* Note: Account creation is asynchronous and may take up to 10 minutes.
|
||||||
|
*
|
||||||
|
* @param params - Account registration parameters
|
||||||
|
* @throws BadRequestException if registration fails
|
||||||
|
*/
|
||||||
|
async registerAccount(params: AccountRegistrationParams): Promise<void> {
|
||||||
|
const { account, planCode, globalIp, priorityFlag } = params;
|
||||||
|
|
||||||
|
// Validate required parameters
|
||||||
|
if (!account || account.length < 11 || account.length > 14) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Invalid phone number (account) for registration - must be 11-14 digits"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!planCode) {
|
||||||
|
throw new BadRequestException("Plan code is required for account registration");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log("Starting MVNO account registration (PA02-01)", {
|
||||||
|
account,
|
||||||
|
accountLength: account.length,
|
||||||
|
planCode,
|
||||||
|
hasGlobalIp: !!globalIp,
|
||||||
|
hasPriorityFlag: !!priorityFlag,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Build payload according to PA02-01 documentation
|
||||||
|
// Note: authKey is added automatically by makeAuthenticatedRequest
|
||||||
|
const payload: FreebitAccountRegistrationRequest = {
|
||||||
|
createType: "new",
|
||||||
|
requestDatas: [
|
||||||
|
{
|
||||||
|
kind: "MVNO",
|
||||||
|
account,
|
||||||
|
planCode,
|
||||||
|
...(globalIp && { globalIp }),
|
||||||
|
...(priorityFlag && { priorityFlag }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// PA02-01 uses form-urlencoded format with json= parameter
|
||||||
|
const response = await this.client.makeAuthenticatedRequest<
|
||||||
|
FreebitAccountRegistrationResponse,
|
||||||
|
FreebitAccountRegistrationRequest
|
||||||
|
>("/master/addAcnt/", payload);
|
||||||
|
|
||||||
|
// Check response for individual account results
|
||||||
|
if (response.responseDatas && response.responseDatas.length > 0) {
|
||||||
|
const accountResult = response.responseDatas[0];
|
||||||
|
if (accountResult.resultCode !== "100") {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Account registration failed for ${account}: result code ${accountResult.resultCode}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log("MVNO account registration successful (PA02-01)", {
|
||||||
|
account,
|
||||||
|
planCode,
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = extractErrorMessage(error);
|
||||||
|
this.logger.error("MVNO account registration failed (PA02-01)", {
|
||||||
|
account,
|
||||||
|
planCode,
|
||||||
|
error: message,
|
||||||
|
});
|
||||||
|
throw new BadRequestException(`Account registration failed: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,213 @@
|
|||||||
|
/**
|
||||||
|
* Freebit Voice Options Service (PA05-05)
|
||||||
|
*
|
||||||
|
* Handles MVNO voice option registration via the Freebit PA05-05 API.
|
||||||
|
* This is called after PA02-01 account registration to configure
|
||||||
|
* voice features like VoiceMail, CallWaiting, WorldCall, etc.
|
||||||
|
*
|
||||||
|
* @see docs/freebit-apis/PA05-05-mvno-voice-option-registration.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Inject, BadRequestException } from "@nestjs/common";
|
||||||
|
import { Logger } from "nestjs-pino";
|
||||||
|
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
|
||||||
|
import { FreebitClientService } from "./freebit-client.service.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identity data required for voice option registration
|
||||||
|
*/
|
||||||
|
export interface VoiceOptionIdentityData {
|
||||||
|
/** Last name in Kanji (UTF-8, max 50 chars) */
|
||||||
|
lastnameKanji: string;
|
||||||
|
/** First name in Kanji (UTF-8, max 50 chars) */
|
||||||
|
firstnameKanji: string;
|
||||||
|
/** Last name in Katakana (full-width, max 50 chars) */
|
||||||
|
lastnameKana: string;
|
||||||
|
/** First name in Katakana (full-width, max 50 chars) */
|
||||||
|
firstnameKana: string;
|
||||||
|
/** Gender: "M" = Male, "F" = Female */
|
||||||
|
gender: "M" | "F";
|
||||||
|
/** Birthday in YYYYMMDD format */
|
||||||
|
birthday: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PA05-05 Voice Option Registration parameters
|
||||||
|
*/
|
||||||
|
export interface VoiceOptionRegistrationParams {
|
||||||
|
/** MSISDN (phone number) - must be already registered via PA02-01 */
|
||||||
|
account: string;
|
||||||
|
/** Enable VoiceMail service */
|
||||||
|
voiceMailEnabled: boolean;
|
||||||
|
/** Enable CallWaiting (Catch Phone) service */
|
||||||
|
callWaitingEnabled: boolean;
|
||||||
|
/** Customer identity data (required) */
|
||||||
|
identificationData: VoiceOptionIdentityData;
|
||||||
|
/** WorldCall credit limit in yen (default: 5000) */
|
||||||
|
worldCallCreditLimit?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PA05-05 Request payload structure
|
||||||
|
*/
|
||||||
|
interface FreebitVoiceOptionRequest {
|
||||||
|
account: string;
|
||||||
|
userConfirmed: "10" | "20";
|
||||||
|
aladinOperated: "10" | "20";
|
||||||
|
talkOption: {
|
||||||
|
voiceMail: "10" | "20";
|
||||||
|
callWaiting: "10" | "20";
|
||||||
|
callTransfer?: "10" | "20";
|
||||||
|
callTransferNoId?: "10" | "20";
|
||||||
|
worldCall: "10" | "20";
|
||||||
|
worldCallCreditLimit?: string;
|
||||||
|
worldWing: "10" | "20";
|
||||||
|
worldWingCreditLimit?: string;
|
||||||
|
};
|
||||||
|
identificationData: {
|
||||||
|
lastnameKanji: string;
|
||||||
|
firstnameKanji: string;
|
||||||
|
lastnameKana: string;
|
||||||
|
firstnameKana: string;
|
||||||
|
gender: string;
|
||||||
|
birthday: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PA05-05 Response structure
|
||||||
|
*/
|
||||||
|
interface FreebitVoiceOptionResponse {
|
||||||
|
resultCode: string;
|
||||||
|
status?: {
|
||||||
|
message?: string;
|
||||||
|
statusCode?: string | number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FreebitVoiceOptionsService {
|
||||||
|
constructor(
|
||||||
|
private readonly client: FreebitClientService,
|
||||||
|
@Inject(Logger) private readonly logger: Logger
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register voice options for an MVNO account (PA05-05)
|
||||||
|
*
|
||||||
|
* This configures voice features for a phone number that was previously
|
||||||
|
* registered via PA02-01. Must be called after account registration.
|
||||||
|
*
|
||||||
|
* Default settings applied:
|
||||||
|
* - WorldCall: Always enabled
|
||||||
|
* - WorldWing: Always disabled
|
||||||
|
*
|
||||||
|
* @param params - Voice option registration parameters
|
||||||
|
* @throws BadRequestException if registration fails
|
||||||
|
*/
|
||||||
|
async registerVoiceOptions(params: VoiceOptionRegistrationParams): Promise<void> {
|
||||||
|
const {
|
||||||
|
account,
|
||||||
|
voiceMailEnabled,
|
||||||
|
callWaitingEnabled,
|
||||||
|
identificationData,
|
||||||
|
worldCallCreditLimit = "5000",
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
// Validate required parameters
|
||||||
|
if (!account || account.length < 11) {
|
||||||
|
throw new BadRequestException("Invalid phone number (account) for voice option registration");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!identificationData) {
|
||||||
|
throw new BadRequestException("Identity data is required for voice option registration");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate identity data
|
||||||
|
if (!identificationData.lastnameKanji || !identificationData.firstnameKanji) {
|
||||||
|
throw new BadRequestException("Name (Kanji) is required for voice option registration");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!identificationData.lastnameKana || !identificationData.firstnameKana) {
|
||||||
|
throw new BadRequestException("Name (Kana) is required for voice option registration");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!identificationData.gender || !["M", "F"].includes(identificationData.gender)) {
|
||||||
|
throw new BadRequestException("Valid gender (M/F) is required for voice option registration");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!identificationData.birthday || !/^\d{8}$/.test(identificationData.birthday)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Birthday in YYYYMMDD format is required for voice option registration"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log("Starting voice option registration (PA05-05)", {
|
||||||
|
account,
|
||||||
|
voiceMailEnabled,
|
||||||
|
callWaitingEnabled,
|
||||||
|
worldCallCreditLimit,
|
||||||
|
hasIdentityData: !!identificationData,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Build payload according to PA05-05 documentation
|
||||||
|
// Note: authKey is added automatically by makeAuthenticatedRequest
|
||||||
|
const payload: FreebitVoiceOptionRequest = {
|
||||||
|
account,
|
||||||
|
userConfirmed: "10", // Always confirmed
|
||||||
|
aladinOperated: "10", // ALADIN operated
|
||||||
|
talkOption: {
|
||||||
|
voiceMail: voiceMailEnabled ? "10" : "20",
|
||||||
|
callWaiting: callWaitingEnabled ? "10" : "20",
|
||||||
|
worldCall: "10", // Always enabled per requirements
|
||||||
|
worldCallCreditLimit,
|
||||||
|
worldWing: "20", // Always disabled per requirements
|
||||||
|
},
|
||||||
|
identificationData: {
|
||||||
|
lastnameKanji: identificationData.lastnameKanji,
|
||||||
|
firstnameKanji: identificationData.firstnameKanji,
|
||||||
|
lastnameKana: identificationData.lastnameKana,
|
||||||
|
firstnameKana: identificationData.firstnameKana,
|
||||||
|
gender: identificationData.gender,
|
||||||
|
birthday: identificationData.birthday,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// PA05-05 uses form-urlencoded format with json= parameter
|
||||||
|
await this.client.makeAuthenticatedRequest<
|
||||||
|
FreebitVoiceOptionResponse,
|
||||||
|
FreebitVoiceOptionRequest
|
||||||
|
>("/mvno/talkoption/addOrder/", payload);
|
||||||
|
|
||||||
|
this.logger.log("Voice option registration successful (PA05-05)", {
|
||||||
|
account,
|
||||||
|
voiceMailEnabled,
|
||||||
|
callWaitingEnabled,
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = extractErrorMessage(error);
|
||||||
|
this.logger.error("Voice option registration failed (PA05-05)", {
|
||||||
|
account,
|
||||||
|
error: message,
|
||||||
|
});
|
||||||
|
throw new BadRequestException(`Voice option registration failed: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format birthday from Date or ISO string to YYYYMMDD
|
||||||
|
*/
|
||||||
|
formatBirthday(date: Date | string | undefined): string {
|
||||||
|
if (!date) return "";
|
||||||
|
|
||||||
|
const d = typeof date === "string" ? new Date(date) : date;
|
||||||
|
if (isNaN(d.getTime())) return "";
|
||||||
|
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}${month}${day}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -134,6 +134,11 @@ export class OrderCdcSubscriber implements OnModuleInit {
|
|||||||
await this.handleActivationStatusChange(payload, orderId);
|
await this.handleActivationStatusChange(payload, orderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for provisioning trigger (Status change to "Approved")
|
||||||
|
if (payload && changedFields.has("Status")) {
|
||||||
|
await this.handleStatusApprovedChange(payload, orderId);
|
||||||
|
}
|
||||||
|
|
||||||
// Cache invalidation - only for customer-facing field changes
|
// Cache invalidation - only for customer-facing field changes
|
||||||
const hasCustomerFacingChange = this.hasCustomerFacingChanges(changedFields);
|
const hasCustomerFacingChange = this.hasCustomerFacingChanges(changedFields);
|
||||||
|
|
||||||
@ -216,6 +221,60 @@ export class OrderCdcSubscriber implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle order status changes to "Approved"
|
||||||
|
*
|
||||||
|
* Enqueues a provisioning job when Status changes to "Approved".
|
||||||
|
* The provisioning processor will fetch the full order from Salesforce
|
||||||
|
* and validate the conditions (SIM_Type__c, Assign_Physical_SIM__c, etc.)
|
||||||
|
*
|
||||||
|
* NOTE: We cannot check SIM_Type__c or Assign_Physical_SIM__c from the CDC payload
|
||||||
|
* because CDC only includes CHANGED fields. If only Status was updated, those fields
|
||||||
|
* will be null in the payload even though they have values on the record.
|
||||||
|
*
|
||||||
|
* The processor handles:
|
||||||
|
* - Physical SIM: Status="Approved" + SIM_Type="Physical SIM" + Assigned_Physical_SIM set
|
||||||
|
* - Standard: Activation_Status__c="Activating"
|
||||||
|
* - Idempotency via WHMCS_Order_ID__c check
|
||||||
|
*/
|
||||||
|
private async handleStatusApprovedChange(
|
||||||
|
payload: Record<string, unknown>,
|
||||||
|
orderId: string
|
||||||
|
): Promise<void> {
|
||||||
|
const status = extractStringField(payload, ["Status"]);
|
||||||
|
|
||||||
|
// Only trigger when status changes to "Approved"
|
||||||
|
if (status !== "Approved") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We intentionally do NOT check SIM_Type__c or Assign_Physical_SIM__c here
|
||||||
|
// because CDC payloads only contain changed fields. The provisioning processor
|
||||||
|
// will fetch the full order and validate all conditions.
|
||||||
|
|
||||||
|
this.logger.log("Enqueuing provisioning job for order status change to Approved", {
|
||||||
|
orderId,
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.provisioningQueue.enqueue({
|
||||||
|
sfOrderId: orderId,
|
||||||
|
idempotencyKey: `cdc-status-approved-${Date.now()}-${orderId}`,
|
||||||
|
correlationId: `cdc-status-approved-${orderId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log("Successfully enqueued provisioning job for Approved status", {
|
||||||
|
orderId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("Failed to enqueue provisioning job for Approved status", {
|
||||||
|
orderId,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// OrderItem CDC Handler
|
// OrderItem CDC Handler
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { OpportunityResolutionService } from "./services/opportunity-resolution.
|
|||||||
import { SalesforceOrderFieldConfigModule } from "./config/salesforce-order-field-config.module.js";
|
import { SalesforceOrderFieldConfigModule } from "./config/salesforce-order-field-config.module.js";
|
||||||
import { SalesforceReadThrottleGuard } from "./guards/salesforce-read-throttle.guard.js";
|
import { SalesforceReadThrottleGuard } from "./guards/salesforce-read-throttle.guard.js";
|
||||||
import { SalesforceWriteThrottleGuard } from "./guards/salesforce-write-throttle.guard.js";
|
import { SalesforceWriteThrottleGuard } from "./guards/salesforce-write-throttle.guard.js";
|
||||||
|
import { SalesforceSIMInventoryService } from "./services/salesforce-sim-inventory.service.js";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [QueueModule, ConfigModule, SalesforceOrderFieldConfigModule],
|
imports: [QueueModule, ConfigModule, SalesforceOrderFieldConfigModule],
|
||||||
@ -21,6 +22,7 @@ import { SalesforceWriteThrottleGuard } from "./guards/salesforce-write-throttle
|
|||||||
SalesforceCaseService,
|
SalesforceCaseService,
|
||||||
SalesforceOpportunityService,
|
SalesforceOpportunityService,
|
||||||
OpportunityResolutionService,
|
OpportunityResolutionService,
|
||||||
|
SalesforceSIMInventoryService,
|
||||||
SalesforceService,
|
SalesforceService,
|
||||||
SalesforceReadThrottleGuard,
|
SalesforceReadThrottleGuard,
|
||||||
SalesforceWriteThrottleGuard,
|
SalesforceWriteThrottleGuard,
|
||||||
@ -34,6 +36,7 @@ import { SalesforceWriteThrottleGuard } from "./guards/salesforce-write-throttle
|
|||||||
SalesforceCaseService,
|
SalesforceCaseService,
|
||||||
SalesforceOpportunityService,
|
SalesforceOpportunityService,
|
||||||
OpportunityResolutionService,
|
OpportunityResolutionService,
|
||||||
|
SalesforceSIMInventoryService,
|
||||||
SalesforceReadThrottleGuard,
|
SalesforceReadThrottleGuard,
|
||||||
SalesforceWriteThrottleGuard,
|
SalesforceWriteThrottleGuard,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -135,21 +135,30 @@ export class SalesforceService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = (await this.connection.query(
|
const soql = `
|
||||||
`SELECT Id, Status, Type, Activation_Status__c, WHMCS_Order_ID__c,
|
SELECT Id, Status, Type, Activation_Status__c, WHMCS_Order_ID__c,
|
||||||
Activation_Error_Code__c, Activation_Error_Message__c,
|
Activation_Error_Code__c, Activation_Error_Message__c,
|
||||||
AccountId, Account.Name
|
AccountId, Account.Name, SIM_Type__c, Assign_Physical_SIM__c
|
||||||
FROM Order
|
FROM Order
|
||||||
WHERE Id = '${orderId}'
|
WHERE Id = '${orderId}'
|
||||||
LIMIT 1`,
|
LIMIT 1
|
||||||
{ label: "orders:integration:getOrder" }
|
`.trim();
|
||||||
)) as { records: SalesforceOrderRecord[]; totalSize: number };
|
|
||||||
|
const result = (await this.connection.query(soql, {
|
||||||
|
label: "orders:integration:getOrder",
|
||||||
|
})) as { records: SalesforceOrderRecord[]; totalSize: number };
|
||||||
|
|
||||||
return result.records?.[0] || null;
|
return result.records?.[0] || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Temporary: Raw console log to see full error
|
||||||
|
console.error(
|
||||||
|
">>> SALESFORCE getOrder ERROR >>>",
|
||||||
|
JSON.stringify(error, Object.getOwnPropertyNames(error as object), 2)
|
||||||
|
);
|
||||||
this.logger.error("Failed to get order from Salesforce", {
|
this.logger.error("Failed to get order from Salesforce", {
|
||||||
orderId,
|
orderId,
|
||||||
error: extractErrorMessage(error),
|
error: extractErrorMessage(error),
|
||||||
|
errorDetails: error instanceof Error ? { name: error.name, stack: error.stack } : error,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,247 @@
|
|||||||
|
/**
|
||||||
|
* Salesforce SIM Inventory Integration Service
|
||||||
|
*
|
||||||
|
* Manages Physical SIM inventory records in Salesforce.
|
||||||
|
* - Query SIM_Inventory__c by ID
|
||||||
|
* - Validate SIM availability status
|
||||||
|
* - Update SIM status after activation
|
||||||
|
*
|
||||||
|
* @see docs/integrations/salesforce/sim-inventory.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Inject } from "@nestjs/common";
|
||||||
|
import { Logger } from "nestjs-pino";
|
||||||
|
import { SalesforceConnection } from "./salesforce-connection.service.js";
|
||||||
|
import { assertSalesforceId } from "../utils/soql.util.js";
|
||||||
|
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
|
||||||
|
import { SimActivationException } from "@bff/core/exceptions/domain-exceptions.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SIM Inventory Status values in Salesforce
|
||||||
|
*/
|
||||||
|
export const SIM_INVENTORY_STATUS = {
|
||||||
|
AVAILABLE: "Available",
|
||||||
|
IN_USE: "In Use",
|
||||||
|
RESERVED: "Reserved",
|
||||||
|
DEACTIVATED: "Deactivated",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type SimInventoryStatus = (typeof SIM_INVENTORY_STATUS)[keyof typeof SIM_INVENTORY_STATUS];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SIM Inventory record from Salesforce
|
||||||
|
*/
|
||||||
|
export interface SimInventoryRecord {
|
||||||
|
id: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
ptNumber: string;
|
||||||
|
oemId?: string;
|
||||||
|
status: SimInventoryStatus;
|
||||||
|
// Note: ICCID__c and IMSI__c fields don't exist on SIM_Inventory__c in this Salesforce org
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw Salesforce SIM_Inventory__c response
|
||||||
|
*/
|
||||||
|
interface SalesforceSIMInventoryResponse {
|
||||||
|
records: Array<{
|
||||||
|
Id: string;
|
||||||
|
Phone_Number__c?: string | null;
|
||||||
|
PT_Number__c?: string | null;
|
||||||
|
OEM_ID__c?: string | null;
|
||||||
|
Status__c?: string | null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SalesforceSIMInventoryService {
|
||||||
|
constructor(
|
||||||
|
private readonly sf: SalesforceConnection,
|
||||||
|
@Inject(Logger) private readonly logger: Logger
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get SIM Inventory record by ID
|
||||||
|
*
|
||||||
|
* @param simInventoryId - Salesforce ID of the SIM_Inventory__c record
|
||||||
|
* @returns SIM Inventory details or null if not found
|
||||||
|
*/
|
||||||
|
async getSimInventoryById(simInventoryId: string): Promise<SimInventoryRecord | null> {
|
||||||
|
const safeId = assertSalesforceId(simInventoryId, "simInventoryId");
|
||||||
|
this.logger.log("Fetching SIM Inventory record", { simInventoryId: safeId });
|
||||||
|
|
||||||
|
// Note: ICCID__c and IMSI__c fields don't exist on SIM_Inventory__c in this org
|
||||||
|
// Only query fields that actually exist
|
||||||
|
const soql = `
|
||||||
|
SELECT Id, Phone_Number__c, PT_Number__c, OEM_ID__c, Status__c
|
||||||
|
FROM SIM_Inventory__c
|
||||||
|
WHERE Id = '${safeId}'
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = (await this.sf.query(soql, {
|
||||||
|
label: "sim-inventory:getById",
|
||||||
|
})) as SalesforceSIMInventoryResponse;
|
||||||
|
|
||||||
|
const record = result.records?.[0];
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
this.logger.warn("SIM Inventory record not found", { simInventoryId: safeId });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log("SIM Inventory record retrieved", {
|
||||||
|
simInventoryId: safeId,
|
||||||
|
status: record.Status__c,
|
||||||
|
hasPhoneNumber: !!record.Phone_Number__c,
|
||||||
|
hasPtNumber: !!record.PT_Number__c,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.mapToSimInventoryRecord(record);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// Temporary: Raw console log to see full Salesforce error
|
||||||
|
console.error(">>> SIM_INVENTORY QUERY ERROR >>>", {
|
||||||
|
simInventoryId: safeId,
|
||||||
|
soql,
|
||||||
|
errorMessage: error instanceof Error ? error.message : String(error),
|
||||||
|
errorName: error instanceof Error ? error.name : "Unknown",
|
||||||
|
errorStack: error instanceof Error ? error.stack : undefined,
|
||||||
|
fullError: JSON.stringify(error, Object.getOwnPropertyNames(error as object), 2),
|
||||||
|
});
|
||||||
|
this.logger.error("Failed to fetch SIM Inventory record", {
|
||||||
|
simInventoryId: safeId,
|
||||||
|
error: extractErrorMessage(error),
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get and validate SIM Inventory for activation
|
||||||
|
*
|
||||||
|
* Fetches the SIM Inventory record and validates:
|
||||||
|
* - Record exists
|
||||||
|
* - Status is "Available"
|
||||||
|
* - PT_Number__c (productNumber) is set
|
||||||
|
* - Phone_Number__c (MSISDN) is set
|
||||||
|
*
|
||||||
|
* @throws SimActivationException if validation fails
|
||||||
|
*/
|
||||||
|
async getAndValidateForActivation(simInventoryId: string): Promise<SimInventoryRecord> {
|
||||||
|
const record = await this.getSimInventoryById(simInventoryId);
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
throw new SimActivationException("SIM Inventory record not found", {
|
||||||
|
simInventoryId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate status is "Available"
|
||||||
|
if (record.status !== SIM_INVENTORY_STATUS.AVAILABLE) {
|
||||||
|
throw new SimActivationException(
|
||||||
|
`SIM is not available for activation. Current status: ${record.status}`,
|
||||||
|
{
|
||||||
|
simInventoryId,
|
||||||
|
currentStatus: record.status,
|
||||||
|
expectedStatus: SIM_INVENTORY_STATUS.AVAILABLE,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate PT Number is set
|
||||||
|
if (!record.ptNumber) {
|
||||||
|
throw new SimActivationException("SIM Inventory record missing PT Number", {
|
||||||
|
simInventoryId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Phone Number is set
|
||||||
|
if (!record.phoneNumber) {
|
||||||
|
throw new SimActivationException("SIM Inventory record missing Phone Number (MSISDN)", {
|
||||||
|
simInventoryId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log("SIM Inventory validated for activation", {
|
||||||
|
simInventoryId,
|
||||||
|
phoneNumber: record.phoneNumber,
|
||||||
|
ptNumber: record.ptNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update SIM Inventory status to "In Use" after successful activation
|
||||||
|
*/
|
||||||
|
async markAsInUse(simInventoryId: string): Promise<void> {
|
||||||
|
const safeId = assertSalesforceId(simInventoryId, "simInventoryId");
|
||||||
|
|
||||||
|
this.logger.log("Marking SIM Inventory as In Use", { simInventoryId: safeId });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.sf.sobject("SIM_Inventory__c").update?.({
|
||||||
|
Id: safeId,
|
||||||
|
Status__c: SIM_INVENTORY_STATUS.IN_USE,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log("SIM Inventory marked as In Use", { simInventoryId: safeId });
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error("Failed to update SIM Inventory status", {
|
||||||
|
simInventoryId: safeId,
|
||||||
|
error: extractErrorMessage(error),
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update SIM Inventory status
|
||||||
|
*
|
||||||
|
* @param simInventoryId - Salesforce ID
|
||||||
|
* @param status - New status value
|
||||||
|
*/
|
||||||
|
async updateStatus(simInventoryId: string, status: SimInventoryStatus): Promise<void> {
|
||||||
|
const safeId = assertSalesforceId(simInventoryId, "simInventoryId");
|
||||||
|
|
||||||
|
this.logger.log("Updating SIM Inventory status", {
|
||||||
|
simInventoryId: safeId,
|
||||||
|
newStatus: status,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.sf.sobject("SIM_Inventory__c").update?.({
|
||||||
|
Id: safeId,
|
||||||
|
Status__c: status,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log("SIM Inventory status updated", {
|
||||||
|
simInventoryId: safeId,
|
||||||
|
newStatus: status,
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error("Failed to update SIM Inventory status", {
|
||||||
|
simInventoryId: safeId,
|
||||||
|
newStatus: status,
|
||||||
|
error: extractErrorMessage(error),
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map raw Salesforce record to domain type
|
||||||
|
*/
|
||||||
|
private mapToSimInventoryRecord(
|
||||||
|
raw: SalesforceSIMInventoryResponse["records"][0]
|
||||||
|
): SimInventoryRecord {
|
||||||
|
return {
|
||||||
|
id: raw.Id,
|
||||||
|
phoneNumber: raw.Phone_Number__c ?? "",
|
||||||
|
ptNumber: raw.PT_Number__c ?? "",
|
||||||
|
oemId: raw.OEM_ID__c ?? undefined,
|
||||||
|
status: (raw.Status__c as SimInventoryStatus) ?? SIM_INVENTORY_STATUS.AVAILABLE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -25,15 +25,27 @@ export class ProvisioningProcessor extends WorkerHost {
|
|||||||
correlationId: job.data.correlationId,
|
correlationId: job.data.correlationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Guard: Only process if Salesforce Order is currently 'Activating'
|
|
||||||
|
|
||||||
const order = await this.salesforceService.getOrder(sfOrderId);
|
const order = await this.salesforceService.getOrder(sfOrderId);
|
||||||
const status = order?.Activation_Status__c ?? "";
|
const activationStatus = order?.Activation_Status__c ?? "";
|
||||||
|
const orderStatus = order?.Status ?? "";
|
||||||
|
const simType = order?.SIM_Type__c ?? "";
|
||||||
|
const assignedPhysicalSim = order?.Assign_Physical_SIM__c ?? "";
|
||||||
const lastErrorCode = order?.Activation_Error_Code__c ?? "";
|
const lastErrorCode = order?.Activation_Error_Code__c ?? "";
|
||||||
if (status !== "Activating") {
|
|
||||||
this.logger.log("Skipping provisioning job: Order not in Activating state", {
|
// Guard: Determine if this is a valid provisioning request
|
||||||
|
// Case 1: Standard flow - Activation_Status__c = "Activating"
|
||||||
|
// Case 2: Physical SIM flow - Status = "Approved" with SIM_Type__c = "Physical SIM"
|
||||||
|
const isStandardActivation = activationStatus === "Activating";
|
||||||
|
const isPhysicalSimApproval =
|
||||||
|
orderStatus === "Approved" && simType === "Physical SIM" && !!assignedPhysicalSim;
|
||||||
|
|
||||||
|
if (!isStandardActivation && !isPhysicalSimApproval) {
|
||||||
|
this.logger.log("Skipping provisioning job: Order not in activatable state", {
|
||||||
sfOrderId,
|
sfOrderId,
|
||||||
currentStatus: status,
|
activationStatus,
|
||||||
|
orderStatus,
|
||||||
|
simType,
|
||||||
|
hasAssignedPhysicalSim: !!assignedPhysicalSim,
|
||||||
});
|
});
|
||||||
return; // Ack + no-op to safely handle duplicate/old events
|
return; // Ack + no-op to safely handle duplicate/old events
|
||||||
}
|
}
|
||||||
@ -42,12 +54,19 @@ export class ProvisioningProcessor extends WorkerHost {
|
|||||||
if (lastErrorCode === "PAYMENT_METHOD_MISSING") {
|
if (lastErrorCode === "PAYMENT_METHOD_MISSING") {
|
||||||
this.logger.log("Skipping provisioning job: Awaiting payment method addition", {
|
this.logger.log("Skipping provisioning job: Awaiting payment method addition", {
|
||||||
sfOrderId,
|
sfOrderId,
|
||||||
currentStatus: status,
|
activationStatus,
|
||||||
lastErrorCode,
|
lastErrorCode,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.log("Proceeding with provisioning", {
|
||||||
|
sfOrderId,
|
||||||
|
isStandardActivation,
|
||||||
|
isPhysicalSimApproval,
|
||||||
|
simType,
|
||||||
|
});
|
||||||
|
|
||||||
// Execute the same orchestration used by the webhook path, but without payload validation
|
// Execute the same orchestration used by the webhook path, but without payload validation
|
||||||
await this.orchestrator.executeFulfillment(sfOrderId, {}, idempotencyKey);
|
await this.orchestrator.executeFulfillment(sfOrderId, {}, idempotencyKey);
|
||||||
this.logger.log("Provisioning job completed", { sfOrderId });
|
this.logger.log("Provisioning job completed", { sfOrderId });
|
||||||
|
|||||||
@ -15,7 +15,10 @@ import { OrdersCacheService } from "./orders-cache.service.js";
|
|||||||
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
import { MappingsService } from "@bff/modules/id-mappings/mappings.service.js";
|
||||||
import { NotificationService } from "@bff/modules/notifications/notifications.service.js";
|
import { NotificationService } from "@bff/modules/notifications/notifications.service.js";
|
||||||
import type { OrderDetails } from "@customer-portal/domain/orders";
|
import type { OrderDetails } from "@customer-portal/domain/orders";
|
||||||
import type { OrderFulfillmentValidationResult } from "@customer-portal/domain/orders/providers";
|
import type {
|
||||||
|
OrderFulfillmentValidationResult,
|
||||||
|
SalesforceOrderRecord,
|
||||||
|
} from "@customer-portal/domain/orders/providers";
|
||||||
import { createOrderNotes, mapOrderToWhmcsItems } from "@customer-portal/domain/orders/providers";
|
import { createOrderNotes, mapOrderToWhmcsItems } from "@customer-portal/domain/orders/providers";
|
||||||
import { OPPORTUNITY_STAGE } from "@customer-portal/domain/opportunity";
|
import { OPPORTUNITY_STAGE } from "@customer-portal/domain/opportunity";
|
||||||
import { NOTIFICATION_SOURCE, NOTIFICATION_TYPE } from "@customer-portal/domain/notifications";
|
import { NOTIFICATION_SOURCE, NOTIFICATION_TYPE } from "@customer-portal/domain/notifications";
|
||||||
@ -320,16 +323,39 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
description: "SIM-specific fulfillment (if applicable)",
|
description: "SIM-specific fulfillment (if applicable)",
|
||||||
execute: this.createTrackedStep(context, "sim_fulfillment", async () => {
|
execute: this.createTrackedStep(context, "sim_fulfillment", async () => {
|
||||||
if (context.orderDetails?.orderType === "SIM") {
|
if (context.orderDetails?.orderType === "SIM") {
|
||||||
const configurations = this.extractConfigurations(payload.configurations);
|
// Extract configurations from payload, with fallback to SF order fields (for CDC flow)
|
||||||
|
const configurations = this.extractConfigurations(
|
||||||
|
payload.configurations,
|
||||||
|
context.validation?.sfOrder
|
||||||
|
);
|
||||||
|
// Extract Physical SIM inventory ID from Salesforce order for Physical SIM activation
|
||||||
|
const assignedPhysicalSimId =
|
||||||
|
typeof context.validation?.sfOrder?.Assign_Physical_SIM__c === "string"
|
||||||
|
? context.validation.sfOrder.Assign_Physical_SIM__c
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Debug: Log the extracted Physical SIM ID
|
||||||
|
this.logger.log("Physical SIM assignment check", {
|
||||||
|
orderId: context.sfOrderId,
|
||||||
|
simType: context.validation?.sfOrder?.SIM_Type__c,
|
||||||
|
assignPhysicalSimRaw: context.validation?.sfOrder?.Assign_Physical_SIM__c,
|
||||||
|
assignPhysicalSimType:
|
||||||
|
typeof context.validation?.sfOrder?.Assign_Physical_SIM__c,
|
||||||
|
assignedPhysicalSimId,
|
||||||
|
});
|
||||||
|
|
||||||
await this.simFulfillmentService.fulfillSimOrder({
|
await this.simFulfillmentService.fulfillSimOrder({
|
||||||
orderDetails: context.orderDetails,
|
orderDetails: context.orderDetails,
|
||||||
configurations,
|
configurations,
|
||||||
|
assignedPhysicalSimId,
|
||||||
});
|
});
|
||||||
return { completed: true as const };
|
return { completed: true as const };
|
||||||
}
|
}
|
||||||
return { skipped: true as const };
|
return { skipped: true as const };
|
||||||
}),
|
}),
|
||||||
critical: false, // SIM fulfillment failure shouldn't rollback the entire order
|
// Physical SIM orders MUST have successful SIM fulfillment - customer can't use service without activated SIM
|
||||||
|
// eSIM orders can retry activation separately if needed since the eSIM profile can be re-downloaded
|
||||||
|
critical: context.validation?.sfOrder?.SIM_Type__c === "Physical SIM",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "sf_success_update",
|
id: "sf_success_update",
|
||||||
@ -499,11 +525,68 @@ export class OrderFulfillmentOrchestrator {
|
|||||||
return steps;
|
return steps;
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractConfigurations(rawConfigurations: unknown): Record<string, unknown> {
|
private extractConfigurations(
|
||||||
|
rawConfigurations: unknown,
|
||||||
|
sfOrder?: SalesforceOrderRecord | null
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const config: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
// Start with payload configurations if provided
|
||||||
if (rawConfigurations && typeof rawConfigurations === "object") {
|
if (rawConfigurations && typeof rawConfigurations === "object") {
|
||||||
return { ...(rawConfigurations as Record<string, unknown>) };
|
Object.assign(config, rawConfigurations as Record<string, unknown>);
|
||||||
}
|
}
|
||||||
return {};
|
|
||||||
|
// Fill in missing fields from Salesforce order (CDC flow fallback)
|
||||||
|
if (sfOrder) {
|
||||||
|
if (!config.simType && sfOrder.SIM_Type__c) {
|
||||||
|
config.simType = sfOrder.SIM_Type__c;
|
||||||
|
}
|
||||||
|
if (!config.eid && sfOrder.EID__c) {
|
||||||
|
config.eid = sfOrder.EID__c;
|
||||||
|
}
|
||||||
|
if (!config.activationType && sfOrder.Activation_Type__c) {
|
||||||
|
config.activationType = sfOrder.Activation_Type__c;
|
||||||
|
}
|
||||||
|
if (!config.scheduledAt && sfOrder.Activation_Scheduled_At__c) {
|
||||||
|
config.scheduledAt = sfOrder.Activation_Scheduled_At__c;
|
||||||
|
}
|
||||||
|
if (!config.mnpPhone && sfOrder.MNP_Phone_Number__c) {
|
||||||
|
config.mnpPhone = sfOrder.MNP_Phone_Number__c;
|
||||||
|
}
|
||||||
|
// MNP fields
|
||||||
|
if (!config.isMnp && sfOrder.MNP_Application__c) {
|
||||||
|
config.isMnp = sfOrder.MNP_Application__c ? "true" : undefined;
|
||||||
|
}
|
||||||
|
if (!config.mnpNumber && sfOrder.MNP_Reservation_Number__c) {
|
||||||
|
config.mnpNumber = sfOrder.MNP_Reservation_Number__c;
|
||||||
|
}
|
||||||
|
if (!config.mnpExpiry && sfOrder.MNP_Expiry_Date__c) {
|
||||||
|
config.mnpExpiry = sfOrder.MNP_Expiry_Date__c;
|
||||||
|
}
|
||||||
|
if (!config.mvnoAccountNumber && sfOrder.MVNO_Account_Number__c) {
|
||||||
|
config.mvnoAccountNumber = sfOrder.MVNO_Account_Number__c;
|
||||||
|
}
|
||||||
|
if (!config.portingFirstName && sfOrder.Porting_FirstName__c) {
|
||||||
|
config.portingFirstName = sfOrder.Porting_FirstName__c;
|
||||||
|
}
|
||||||
|
if (!config.portingLastName && sfOrder.Porting_LastName__c) {
|
||||||
|
config.portingLastName = sfOrder.Porting_LastName__c;
|
||||||
|
}
|
||||||
|
if (!config.portingFirstNameKatakana && sfOrder.Porting_FirstName_Katakana__c) {
|
||||||
|
config.portingFirstNameKatakana = sfOrder.Porting_FirstName_Katakana__c;
|
||||||
|
}
|
||||||
|
if (!config.portingLastNameKatakana && sfOrder.Porting_LastName_Katakana__c) {
|
||||||
|
config.portingLastNameKatakana = sfOrder.Porting_LastName_Katakana__c;
|
||||||
|
}
|
||||||
|
if (!config.portingGender && sfOrder.Porting_Gender__c) {
|
||||||
|
config.portingGender = sfOrder.Porting_Gender__c;
|
||||||
|
}
|
||||||
|
if (!config.portingDateOfBirth && sfOrder.Porting_DateOfBirth__c) {
|
||||||
|
config.portingDateOfBirth = sfOrder.Porting_DateOfBirth__c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async invalidateOrderCaches(orderId: string, accountId?: string | null): Promise<void> {
|
private async invalidateOrderCaches(orderId: string, accountId?: string | null): Promise<void> {
|
||||||
|
|||||||
@ -1,34 +1,85 @@
|
|||||||
import { Injectable, Inject } from "@nestjs/common";
|
import { Injectable, Inject } from "@nestjs/common";
|
||||||
import { Logger } from "nestjs-pino";
|
import { Logger } from "nestjs-pino";
|
||||||
import { FreebitOrchestratorService } from "@bff/integrations/freebit/services/freebit-orchestrator.service.js";
|
import { FreebitOrchestratorService } from "@bff/integrations/freebit/services/freebit-orchestrator.service.js";
|
||||||
|
import { FreebitAccountRegistrationService } from "@bff/integrations/freebit/services/freebit-account-registration.service.js";
|
||||||
|
import { FreebitVoiceOptionsService } from "@bff/integrations/freebit/services/freebit-voice-options.service.js";
|
||||||
|
import { SalesforceSIMInventoryService } from "@bff/integrations/salesforce/services/salesforce-sim-inventory.service.js";
|
||||||
import type { OrderDetails, OrderItemDetails } from "@customer-portal/domain/orders";
|
import type { OrderDetails, OrderItemDetails } from "@customer-portal/domain/orders";
|
||||||
|
import { mapProductToFreebitPlanCode } from "@customer-portal/domain/sim";
|
||||||
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
|
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
|
||||||
import {
|
import {
|
||||||
SimActivationException,
|
SimActivationException,
|
||||||
OrderValidationException,
|
OrderValidationException,
|
||||||
} from "@bff/core/exceptions/domain-exceptions.js";
|
} from "@bff/core/exceptions/domain-exceptions.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact identity data for PA05-05 voice option registration
|
||||||
|
*/
|
||||||
|
export interface ContactIdentityData {
|
||||||
|
firstnameKanji: string;
|
||||||
|
lastnameKanji: string;
|
||||||
|
firstnameKana: string;
|
||||||
|
lastnameKana: string;
|
||||||
|
gender: "M" | "F";
|
||||||
|
birthday: string; // YYYYMMDD format
|
||||||
|
}
|
||||||
|
|
||||||
export interface SimFulfillmentRequest {
|
export interface SimFulfillmentRequest {
|
||||||
orderDetails: OrderDetails;
|
orderDetails: OrderDetails;
|
||||||
configurations: Record<string, unknown>;
|
configurations: Record<string, unknown>;
|
||||||
|
/** Salesforce ID of the assigned Physical SIM (from Assign_Physical_SIM__c) */
|
||||||
|
assignedPhysicalSimId?: string;
|
||||||
|
/** Voice Mail enabled from Order.SIM_Voice_Mail__c */
|
||||||
|
voiceMailEnabled?: boolean;
|
||||||
|
/** Call Waiting enabled from Order.SIM_Call_Waiting__c */
|
||||||
|
callWaitingEnabled?: boolean;
|
||||||
|
/** Contact identity data for PA05-05 */
|
||||||
|
contactIdentity?: ContactIdentityData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SimFulfillmentService {
|
export class SimFulfillmentService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly freebit: FreebitOrchestratorService,
|
private readonly freebit: FreebitOrchestratorService,
|
||||||
|
private readonly freebitAccountReg: FreebitAccountRegistrationService,
|
||||||
|
private readonly freebitVoiceOptions: FreebitVoiceOptionsService,
|
||||||
|
private readonly simInventory: SalesforceSIMInventoryService,
|
||||||
@Inject(Logger) private readonly logger: Logger
|
@Inject(Logger) private readonly logger: Logger
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async fulfillSimOrder(request: SimFulfillmentRequest): Promise<void> {
|
async fulfillSimOrder(request: SimFulfillmentRequest): Promise<void> {
|
||||||
const { orderDetails, configurations } = request;
|
const {
|
||||||
|
orderDetails,
|
||||||
|
configurations,
|
||||||
|
assignedPhysicalSimId,
|
||||||
|
voiceMailEnabled = false,
|
||||||
|
callWaitingEnabled = false,
|
||||||
|
contactIdentity,
|
||||||
|
} = request;
|
||||||
|
|
||||||
|
const simType = this.readEnum(configurations.simType, ["eSIM", "Physical SIM"]);
|
||||||
|
|
||||||
this.logger.log("Starting SIM fulfillment", {
|
this.logger.log("Starting SIM fulfillment", {
|
||||||
orderId: orderDetails.id,
|
orderId: orderDetails.id,
|
||||||
orderType: orderDetails.orderType,
|
orderType: orderDetails.orderType,
|
||||||
|
simType: simType ?? "(not set)",
|
||||||
|
hasAssignedPhysicalSim: !!assignedPhysicalSimId,
|
||||||
|
voiceMailEnabled,
|
||||||
|
callWaitingEnabled,
|
||||||
|
hasContactIdentity: !!contactIdentity,
|
||||||
});
|
});
|
||||||
|
|
||||||
const simType = this.readEnum(configurations.simType, ["eSIM", "Physical SIM"]) ?? "eSIM";
|
// Validate SIM type is explicitly set - don't default to eSIM
|
||||||
|
if (!simType) {
|
||||||
|
throw new SimActivationException(
|
||||||
|
"SIM Type must be explicitly set to 'eSIM' or 'Physical SIM'",
|
||||||
|
{
|
||||||
|
orderId: orderDetails.id,
|
||||||
|
configuredSimType: configurations.simType,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const eid = this.readString(configurations.eid);
|
const eid = this.readString(configurations.eid);
|
||||||
const activationType =
|
const activationType =
|
||||||
this.readEnum(configurations.activationType, ["Immediate", "Scheduled"]) ?? "Immediate";
|
this.readEnum(configurations.activationType, ["Immediate", "Scheduled"]) ?? "Immediate";
|
||||||
@ -48,6 +99,7 @@ export class SimFulfillmentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const planSku = simPlanItem.product?.sku;
|
const planSku = simPlanItem.product?.sku;
|
||||||
|
const planName = simPlanItem.product?.name;
|
||||||
if (!planSku) {
|
if (!planSku) {
|
||||||
throw new OrderValidationException("SIM plan SKU not found", {
|
throw new OrderValidationException("SIM plan SKU not found", {
|
||||||
orderId: orderDetails.id,
|
orderId: orderDetails.id,
|
||||||
@ -55,143 +107,263 @@ export class SimFulfillmentService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (simType === "eSIM" && (!eid || eid.length < 15)) {
|
|
||||||
throw new SimActivationException("EID is required for eSIM and must be valid", {
|
|
||||||
orderId: orderDetails.id,
|
|
||||||
simType,
|
|
||||||
eidLength: eid?.length,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!phoneNumber) {
|
|
||||||
throw new SimActivationException("Phone number is required for SIM activation", {
|
|
||||||
orderId: orderDetails.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (simType === "eSIM") {
|
if (simType === "eSIM") {
|
||||||
if (!eid) {
|
// eSIM activation flow
|
||||||
throw new SimActivationException("EID is required for eSIM activation", {
|
if (!eid || eid.length < 15) {
|
||||||
|
throw new SimActivationException("EID is required for eSIM and must be valid", {
|
||||||
|
orderId: orderDetails.id,
|
||||||
|
simType,
|
||||||
|
eidLength: eid?.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!phoneNumber) {
|
||||||
|
throw new SimActivationException("Phone number is required for eSIM activation", {
|
||||||
orderId: orderDetails.id,
|
orderId: orderDetails.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await this.activateSim({
|
|
||||||
|
await this.activateEsim({
|
||||||
account: phoneNumber,
|
account: phoneNumber,
|
||||||
eid,
|
eid,
|
||||||
planSku,
|
planSku,
|
||||||
simType: "eSIM",
|
|
||||||
activationType,
|
activationType,
|
||||||
scheduledAt,
|
scheduledAt,
|
||||||
mnp,
|
mnp,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
await this.activateSim({
|
this.logger.log("eSIM fulfillment completed successfully", {
|
||||||
|
orderId: orderDetails.id,
|
||||||
account: phoneNumber,
|
account: phoneNumber,
|
||||||
planSku,
|
planSku,
|
||||||
simType: "Physical SIM",
|
});
|
||||||
activationType,
|
} else {
|
||||||
scheduledAt,
|
// Physical SIM activation flow (PA02-01 + PA05-05)
|
||||||
mnp,
|
if (!assignedPhysicalSimId) {
|
||||||
|
throw new SimActivationException(
|
||||||
|
"Physical SIM requires an assigned SIM from inventory (Assign_Physical_SIM__c)",
|
||||||
|
{ orderId: orderDetails.id }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.activatePhysicalSim({
|
||||||
|
orderId: orderDetails.id,
|
||||||
|
simInventoryId: assignedPhysicalSimId,
|
||||||
|
planSku,
|
||||||
|
planName,
|
||||||
|
voiceMailEnabled,
|
||||||
|
callWaitingEnabled,
|
||||||
|
contactIdentity,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log("Physical SIM fulfillment completed successfully", {
|
||||||
|
orderId: orderDetails.id,
|
||||||
|
simInventoryId: assignedPhysicalSimId,
|
||||||
|
planSku,
|
||||||
|
voiceMailEnabled,
|
||||||
|
callWaitingEnabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log("SIM fulfillment completed successfully", {
|
|
||||||
orderId: orderDetails.id,
|
|
||||||
account: phoneNumber,
|
|
||||||
planSku,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async activateSim(
|
/**
|
||||||
params:
|
* Activate eSIM via Freebit PA05-41 API
|
||||||
| {
|
*/
|
||||||
account: string;
|
private async activateEsim(params: {
|
||||||
eid: string;
|
account: string;
|
||||||
planSku: string;
|
eid: string;
|
||||||
simType: "eSIM";
|
planSku: string;
|
||||||
activationType: "Immediate" | "Scheduled";
|
activationType: "Immediate" | "Scheduled";
|
||||||
scheduledAt?: string;
|
scheduledAt?: string;
|
||||||
mnp?: {
|
mnp?: {
|
||||||
reserveNumber?: string;
|
reserveNumber?: string;
|
||||||
reserveExpireDate?: string;
|
reserveExpireDate?: string;
|
||||||
account?: string;
|
account?: string;
|
||||||
firstnameKanji?: string;
|
firstnameKanji?: string;
|
||||||
lastnameKanji?: string;
|
lastnameKanji?: string;
|
||||||
firstnameZenKana?: string;
|
firstnameZenKana?: string;
|
||||||
lastnameZenKana?: string;
|
lastnameZenKana?: string;
|
||||||
gender?: string;
|
gender?: string;
|
||||||
birthday?: string;
|
birthday?: string;
|
||||||
};
|
};
|
||||||
}
|
}): Promise<void> {
|
||||||
| {
|
const { account, eid, planSku, activationType, scheduledAt, mnp } = params;
|
||||||
account: string;
|
|
||||||
eid?: string;
|
|
||||||
planSku: string;
|
|
||||||
simType: "Physical SIM";
|
|
||||||
activationType: "Immediate" | "Scheduled";
|
|
||||||
scheduledAt?: string;
|
|
||||||
mnp?: {
|
|
||||||
reserveNumber?: string;
|
|
||||||
reserveExpireDate?: string;
|
|
||||||
account?: string;
|
|
||||||
firstnameKanji?: string;
|
|
||||||
lastnameKanji?: string;
|
|
||||||
firstnameZenKana?: string;
|
|
||||||
lastnameZenKana?: string;
|
|
||||||
gender?: string;
|
|
||||||
birthday?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
): Promise<void> {
|
|
||||||
const { account, planSku, simType, activationType, scheduledAt, mnp } = params;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (simType === "eSIM") {
|
await this.freebit.activateEsimAccountNew({
|
||||||
const { eid } = params;
|
account,
|
||||||
await this.freebit.activateEsimAccountNew({
|
eid,
|
||||||
account,
|
planCode: planSku,
|
||||||
eid,
|
contractLine: "5G",
|
||||||
planCode: planSku,
|
shipDate: activationType === "Scheduled" ? scheduledAt : undefined,
|
||||||
contractLine: "5G",
|
mnp:
|
||||||
shipDate: activationType === "Scheduled" ? scheduledAt : undefined,
|
mnp && mnp.reserveNumber && mnp.reserveExpireDate
|
||||||
mnp:
|
|
||||||
mnp && mnp.reserveNumber && mnp.reserveExpireDate
|
|
||||||
? {
|
|
||||||
reserveNumber: mnp.reserveNumber,
|
|
||||||
reserveExpireDate: mnp.reserveExpireDate,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
identity: mnp
|
|
||||||
? {
|
? {
|
||||||
firstnameKanji: mnp.firstnameKanji,
|
reserveNumber: mnp.reserveNumber,
|
||||||
lastnameKanji: mnp.lastnameKanji,
|
reserveExpireDate: mnp.reserveExpireDate,
|
||||||
firstnameZenKana: mnp.firstnameZenKana,
|
|
||||||
lastnameZenKana: mnp.lastnameZenKana,
|
|
||||||
gender: mnp.gender,
|
|
||||||
birthday: mnp.birthday,
|
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
identity: mnp
|
||||||
|
? {
|
||||||
|
firstnameKanji: mnp.firstnameKanji,
|
||||||
|
lastnameKanji: mnp.lastnameKanji,
|
||||||
|
firstnameZenKana: mnp.firstnameZenKana,
|
||||||
|
lastnameZenKana: mnp.lastnameZenKana,
|
||||||
|
gender: mnp.gender,
|
||||||
|
birthday: mnp.birthday,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
this.logger.log("eSIM activated successfully", {
|
this.logger.log("eSIM activated successfully", {
|
||||||
account,
|
|
||||||
planSku,
|
|
||||||
scheduled: activationType === "Scheduled",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await this.freebit.topUpSim(account, 0, {
|
|
||||||
scheduledAt: activationType === "Scheduled" ? scheduledAt : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log("Physical SIM activation scheduled", {
|
|
||||||
account,
|
|
||||||
planSku,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
this.logger.error("SIM activation failed", {
|
|
||||||
account,
|
account,
|
||||||
planSku,
|
planSku,
|
||||||
|
scheduled: activationType === "Scheduled",
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error("eSIM activation failed", {
|
||||||
|
account,
|
||||||
|
planSku,
|
||||||
|
error: extractErrorMessage(error),
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate Physical SIM via Freebit PA02-01 + PA05-05 APIs
|
||||||
|
*
|
||||||
|
* New Flow (replaces PA05-33 OTA):
|
||||||
|
* 1. Fetch SIM Inventory details from Salesforce
|
||||||
|
* 2. Validate SIM status is "Available"
|
||||||
|
* 3. Map product SKU to Freebit plan code
|
||||||
|
* 4. Call Freebit PA02-01 (Account Registration) to create MVNO account
|
||||||
|
* 5. Call Freebit PA05-05 (Voice Options) to configure voice features
|
||||||
|
* 6. Update SIM Inventory status to "In Use"
|
||||||
|
*
|
||||||
|
* Note: PA02-01 is asynchronous and may take up to 10 minutes to process.
|
||||||
|
* The account must be fully registered before PA05-05 can be called.
|
||||||
|
*/
|
||||||
|
private async activatePhysicalSim(params: {
|
||||||
|
orderId: string;
|
||||||
|
simInventoryId: string;
|
||||||
|
planSku: string;
|
||||||
|
planName?: string;
|
||||||
|
voiceMailEnabled: boolean;
|
||||||
|
callWaitingEnabled: boolean;
|
||||||
|
contactIdentity?: ContactIdentityData;
|
||||||
|
}): Promise<void> {
|
||||||
|
const {
|
||||||
|
orderId,
|
||||||
|
simInventoryId,
|
||||||
|
planSku,
|
||||||
|
planName,
|
||||||
|
voiceMailEnabled,
|
||||||
|
callWaitingEnabled,
|
||||||
|
contactIdentity,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
this.logger.log("Starting Physical SIM activation (PA02-01 + PA05-05)", {
|
||||||
|
orderId,
|
||||||
|
simInventoryId,
|
||||||
|
planSku,
|
||||||
|
voiceMailEnabled,
|
||||||
|
callWaitingEnabled,
|
||||||
|
hasContactIdentity: !!contactIdentity,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 1 & 2: Fetch and validate SIM Inventory
|
||||||
|
const simRecord = await this.simInventory.getAndValidateForActivation(simInventoryId);
|
||||||
|
|
||||||
|
// Step 3: Map product to Freebit plan code
|
||||||
|
const planCode = mapProductToFreebitPlanCode(planSku, planName);
|
||||||
|
if (!planCode) {
|
||||||
|
throw new SimActivationException(
|
||||||
|
`Unable to map product to Freebit plan code. SKU: ${planSku}, Name: ${planName}`,
|
||||||
|
{ orderId, simInventoryId, planSku, planName }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use phone number from SIM inventory
|
||||||
|
const accountPhoneNumber = simRecord.phoneNumber;
|
||||||
|
|
||||||
|
this.logger.log("Physical SIM inventory validated", {
|
||||||
|
orderId,
|
||||||
|
simInventoryId,
|
||||||
|
accountPhoneNumber,
|
||||||
|
ptNumber: simRecord.ptNumber,
|
||||||
|
planCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 4: Call Freebit PA02-01 (Account Registration)
|
||||||
|
this.logger.log("Calling PA02-01 Account Registration", {
|
||||||
|
orderId,
|
||||||
|
account: accountPhoneNumber,
|
||||||
|
planCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.freebitAccountReg.registerAccount({
|
||||||
|
account: accountPhoneNumber,
|
||||||
|
planCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log("PA02-01 Account Registration successful", {
|
||||||
|
orderId,
|
||||||
|
account: accountPhoneNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 5: Call Freebit PA05-05 (Voice Options Registration)
|
||||||
|
// Only call if we have contact identity data
|
||||||
|
if (contactIdentity) {
|
||||||
|
this.logger.log("Calling PA05-05 Voice Options Registration", {
|
||||||
|
orderId,
|
||||||
|
account: accountPhoneNumber,
|
||||||
|
voiceMailEnabled,
|
||||||
|
callWaitingEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.freebitVoiceOptions.registerVoiceOptions({
|
||||||
|
account: accountPhoneNumber,
|
||||||
|
voiceMailEnabled,
|
||||||
|
callWaitingEnabled,
|
||||||
|
identificationData: {
|
||||||
|
lastnameKanji: contactIdentity.lastnameKanji,
|
||||||
|
firstnameKanji: contactIdentity.firstnameKanji,
|
||||||
|
lastnameKana: contactIdentity.lastnameKana,
|
||||||
|
firstnameKana: contactIdentity.firstnameKana,
|
||||||
|
gender: contactIdentity.gender,
|
||||||
|
birthday: contactIdentity.birthday,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log("PA05-05 Voice Options Registration successful", {
|
||||||
|
orderId,
|
||||||
|
account: accountPhoneNumber,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.logger.warn("Skipping PA05-05: No contact identity data provided", {
|
||||||
|
orderId,
|
||||||
|
account: accountPhoneNumber,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: Update SIM Inventory status to "In Use"
|
||||||
|
await this.simInventory.markAsInUse(simInventoryId);
|
||||||
|
|
||||||
|
this.logger.log("Physical SIM activated successfully", {
|
||||||
|
orderId,
|
||||||
|
simInventoryId,
|
||||||
|
accountPhoneNumber,
|
||||||
|
planCode,
|
||||||
|
voiceMailEnabled,
|
||||||
|
callWaitingEnabled,
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error("Physical SIM activation failed", {
|
||||||
|
orderId,
|
||||||
|
simInventoryId,
|
||||||
|
phoneNumber: simRecord.phoneNumber,
|
||||||
error: extractErrorMessage(error),
|
error: extractErrorMessage(error),
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@ -95,11 +95,11 @@ export const defaultSalesforceOrderFieldMap: SalesforceOrderFieldMap = {
|
|||||||
mnpExpiry: "MNP_Expiry_Date__c",
|
mnpExpiry: "MNP_Expiry_Date__c",
|
||||||
mnpPhone: "MNP_Phone_Number__c",
|
mnpPhone: "MNP_Phone_Number__c",
|
||||||
mvnoAccountNumber: "MVNO_Account_Number__c",
|
mvnoAccountNumber: "MVNO_Account_Number__c",
|
||||||
portingDateOfBirth: "Porting_Date_Of_Birth__c",
|
portingDateOfBirth: "Porting_DateOfBirth__c",
|
||||||
portingFirstName: "Porting_First_Name__c",
|
portingFirstName: "Porting_FirstName__c",
|
||||||
portingLastName: "Porting_Last_Name__c",
|
portingLastName: "Porting_LastName__c",
|
||||||
portingFirstNameKatakana: "Porting_First_Name_Katakana__c",
|
portingFirstNameKatakana: "Porting_FirstName_Katakana__c",
|
||||||
portingLastNameKatakana: "Porting_Last_Name_Katakana__c",
|
portingLastNameKatakana: "Porting_LastName_Katakana__c",
|
||||||
portingGender: "Porting_Gender__c",
|
portingGender: "Porting_Gender__c",
|
||||||
},
|
},
|
||||||
orderItem: {
|
orderItem: {
|
||||||
|
|||||||
@ -69,6 +69,7 @@ export const salesforceOrderRecordSchema = z.object({
|
|||||||
SIM_Voice_Mail__c: z.boolean().nullable().optional(),
|
SIM_Voice_Mail__c: z.boolean().nullable().optional(),
|
||||||
SIM_Call_Waiting__c: z.boolean().nullable().optional(),
|
SIM_Call_Waiting__c: z.boolean().nullable().optional(),
|
||||||
EID__c: z.string().nullable().optional(),
|
EID__c: z.string().nullable().optional(),
|
||||||
|
Assign_Physical_SIM__c: z.string().nullable().optional(), // Lookup to SIM_Inventory__c
|
||||||
|
|
||||||
// MNP (Mobile Number Portability) fields
|
// MNP (Mobile Number Portability) fields
|
||||||
MNP_Application__c: z.string().nullable().optional(),
|
MNP_Application__c: z.string().nullable().optional(),
|
||||||
@ -76,11 +77,11 @@ export const salesforceOrderRecordSchema = z.object({
|
|||||||
MNP_Expiry_Date__c: z.string().nullable().optional(),
|
MNP_Expiry_Date__c: z.string().nullable().optional(),
|
||||||
MNP_Phone_Number__c: z.string().nullable().optional(),
|
MNP_Phone_Number__c: z.string().nullable().optional(),
|
||||||
MVNO_Account_Number__c: z.string().nullable().optional(),
|
MVNO_Account_Number__c: z.string().nullable().optional(),
|
||||||
Porting_Date_Of_Birth__c: z.string().nullable().optional(),
|
Porting_DateOfBirth__c: z.string().nullable().optional(),
|
||||||
Porting_First_Name__c: z.string().nullable().optional(),
|
Porting_FirstName__c: z.string().nullable().optional(),
|
||||||
Porting_Last_Name__c: z.string().nullable().optional(),
|
Porting_LastName__c: z.string().nullable().optional(),
|
||||||
Porting_First_Name_Katakana__c: z.string().nullable().optional(),
|
Porting_FirstName_Katakana__c: z.string().nullable().optional(),
|
||||||
Porting_Last_Name_Katakana__c: z.string().nullable().optional(),
|
Porting_LastName_Katakana__c: z.string().nullable().optional(),
|
||||||
Porting_Gender__c: z.string().nullable().optional(),
|
Porting_Gender__c: z.string().nullable().optional(),
|
||||||
|
|
||||||
// Billing address snapshot fields (standard Salesforce Order columns)
|
// Billing address snapshot fields (standard Salesforce Order columns)
|
||||||
|
|||||||
@ -82,3 +82,52 @@ export function buildSimFeaturesUpdatePayload(
|
|||||||
|
|
||||||
return Object.keys(payload).length > 0 ? payload : null;
|
return Object.keys(payload).length > 0 ? payload : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plan code mapping from product SKU/name to Freebit plan code
|
||||||
|
* Maps common data tiers: 5GB, 10GB, 25GB, 50GB
|
||||||
|
*/
|
||||||
|
const PLAN_CODE_MAPPING: Record<string, string> = {
|
||||||
|
"5": "PASI_5G",
|
||||||
|
"10": "PASI_10G",
|
||||||
|
"25": "PASI_25G",
|
||||||
|
"50": "PASI_50G",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a product SKU or name to the corresponding Freebit plan code.
|
||||||
|
*
|
||||||
|
* Extracts the data tier (e.g., "50" from "SIM Data+Voice 50GB" or "sim-50gb")
|
||||||
|
* and maps it to the appropriate PASI plan code.
|
||||||
|
*
|
||||||
|
* @param productSku - The product SKU (e.g., "sim-data-voice-50gb")
|
||||||
|
* @param productName - The product name (e.g., "SIM Data+Voice 50GB")
|
||||||
|
* @returns The Freebit plan code (e.g., "PASI_50G") or null if not mappable
|
||||||
|
*/
|
||||||
|
export function mapProductToFreebitPlanCode(
|
||||||
|
productSku?: string,
|
||||||
|
productName?: string
|
||||||
|
): string | null {
|
||||||
|
// Try to extract data tier from SKU or name
|
||||||
|
const source = productSku || productName || "";
|
||||||
|
|
||||||
|
// Match patterns like "50GB", "50G", "50gb", or just "50" in context of GB
|
||||||
|
const gbMatch = source.match(/(\d+)\s*G(?:B)?/i);
|
||||||
|
if (gbMatch?.[1]) {
|
||||||
|
const tier = gbMatch[1];
|
||||||
|
if (tier in PLAN_CODE_MAPPING) {
|
||||||
|
return PLAN_CODE_MAPPING[tier];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try matching standalone numbers in SKU patterns like "sim-50gb"
|
||||||
|
const skuMatch = source.match(/[-_](\d+)[-_]?(?:gb|g)?/i);
|
||||||
|
if (skuMatch?.[1]) {
|
||||||
|
const tier = skuMatch[1];
|
||||||
|
if (tier in PLAN_CODE_MAPPING) {
|
||||||
|
return PLAN_CODE_MAPPING[tier];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export {
|
|||||||
SIM_PLAN_OPTIONS,
|
SIM_PLAN_OPTIONS,
|
||||||
getSimPlanLabel,
|
getSimPlanLabel,
|
||||||
buildSimFeaturesUpdatePayload,
|
buildSimFeaturesUpdatePayload,
|
||||||
|
mapProductToFreebitPlanCode,
|
||||||
} from "./helpers.js";
|
} from "./helpers.js";
|
||||||
export type { SimPlanCode } from "./contract.js";
|
export type { SimPlanCode } from "./contract.js";
|
||||||
export type { SimPlanOption, SimFeatureToggleSnapshot } from "./helpers.js";
|
export type { SimPlanOption, SimFeatureToggleSnapshot } from "./helpers.js";
|
||||||
|
|||||||
@ -22,6 +22,8 @@ export const schemas = {
|
|||||||
esimAddAccount: Requests.freebitEsimAddAccountRequestSchema,
|
esimAddAccount: Requests.freebitEsimAddAccountRequestSchema,
|
||||||
auth: Requests.freebitAuthRequestSchema,
|
auth: Requests.freebitAuthRequestSchema,
|
||||||
cancelAccount: Requests.freebitCancelAccountRequestSchema,
|
cancelAccount: Requests.freebitCancelAccountRequestSchema,
|
||||||
|
otaActivation: Requests.freebitOtaActivationRequestSchema,
|
||||||
|
otaActivationResponse: Requests.freebitOtaActivationResponseSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const raw = RawTypes;
|
export const raw = RawTypes;
|
||||||
@ -41,6 +43,8 @@ export type CancelPlanRequest = Requests.FreebitCancelPlanRequest;
|
|||||||
export type CancelPlanApiRequest = Requests.FreebitCancelPlanApiRequest;
|
export type CancelPlanApiRequest = Requests.FreebitCancelPlanApiRequest;
|
||||||
export type CancelAccountRequest = Requests.FreebitCancelAccountRequest;
|
export type CancelAccountRequest = Requests.FreebitCancelAccountRequest;
|
||||||
export type AuthRequest = Requests.FreebitAuthRequest;
|
export type AuthRequest = Requests.FreebitAuthRequest;
|
||||||
|
export type OtaActivationRequest = Requests.FreebitOtaActivationRequest;
|
||||||
|
export type OtaActivationResponse = Requests.FreebitOtaActivationResponse;
|
||||||
export type TopUpResponse = ReturnType<typeof Mapper.transformFreebitTopUpResponse>;
|
export type TopUpResponse = ReturnType<typeof Mapper.transformFreebitTopUpResponse>;
|
||||||
export type AddSpecResponse = ReturnType<typeof Mapper.transformFreebitAddSpecResponse>;
|
export type AddSpecResponse = ReturnType<typeof Mapper.transformFreebitAddSpecResponse>;
|
||||||
export type PlanChangeResponse = ReturnType<typeof Mapper.transformFreebitPlanChangeResponse>;
|
export type PlanChangeResponse = ReturnType<typeof Mapper.transformFreebitPlanChangeResponse>;
|
||||||
|
|||||||
@ -288,3 +288,61 @@ export type FreebitQuotaHistoryResponse = z.infer<typeof freebitQuotaHistoryResp
|
|||||||
export type FreebitEsimAddAccountRequest = z.infer<typeof freebitEsimAddAccountRequestSchema>;
|
export type FreebitEsimAddAccountRequest = z.infer<typeof freebitEsimAddAccountRequestSchema>;
|
||||||
export type FreebitAuthRequest = z.infer<typeof freebitAuthRequestSchema>;
|
export type FreebitAuthRequest = z.infer<typeof freebitAuthRequestSchema>;
|
||||||
export type FreebitCancelAccountRequest = z.infer<typeof freebitCancelAccountRequestSchema>;
|
export type FreebitCancelAccountRequest = z.infer<typeof freebitCancelAccountRequestSchema>;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Physical SIM OTA Activation (PA05-33)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freebit OTA Account Activation Request Schema
|
||||||
|
* PA05-33 API endpoint: /mvno/ota/addAcnt/
|
||||||
|
* Used for Physical SIM activation via OTA (Over-The-Air)
|
||||||
|
*/
|
||||||
|
export const freebitOtaActivationRequestSchema = z.object({
|
||||||
|
authKey: z.string().min(1, "Auth key is required"),
|
||||||
|
aladinOperated: z.enum(["10", "20"]).default("10"), // 10: issue profile, 20: no-issue
|
||||||
|
createType: z.enum(["new", "reissue"]).default("new"),
|
||||||
|
account: z.string().min(1, "Account (MSISDN) is required"),
|
||||||
|
productNumber: z.string().min(1, "Product number (PT) is required"),
|
||||||
|
simkind: z.string().optional(), // Physical SIM kind (e.g., '3MS', '3MR')
|
||||||
|
planCode: z.string().optional(),
|
||||||
|
contractLine: z.enum(["4G", "5G"]).optional(),
|
||||||
|
size: z.enum(["standard", "micro", "nano"]).default("nano"),
|
||||||
|
shipDate: z
|
||||||
|
.string()
|
||||||
|
.regex(/^\d{8}$/, "Ship date must be in YYYYMMDD format")
|
||||||
|
.optional(),
|
||||||
|
deliveryCode: z.string().optional(), // OEM ID code
|
||||||
|
addKind: z.enum(["N", "M"]).default("N"), // N: New, M: MNP
|
||||||
|
mnp: z
|
||||||
|
.object({
|
||||||
|
reserveNumber: z.string().min(1, "MNP reserve number is required"),
|
||||||
|
reserveExpireDate: z.string().regex(/^\d{8}$/, "Reserve expire date must be YYYYMMDD"),
|
||||||
|
account: z.string().optional(),
|
||||||
|
firstnameKanji: z.string().optional(),
|
||||||
|
lastnameKanji: z.string().optional(),
|
||||||
|
firstnameZenKana: z.string().optional(),
|
||||||
|
lastnameZenKana: z.string().optional(),
|
||||||
|
gender: z.enum(["M", "F"]).optional(),
|
||||||
|
birthday: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freebit OTA Account Activation Response Schema
|
||||||
|
*/
|
||||||
|
export const freebitOtaActivationResponseSchema = z.object({
|
||||||
|
resultCode: z.string(),
|
||||||
|
resultMessage: z.string().optional(),
|
||||||
|
status: z
|
||||||
|
.object({
|
||||||
|
statusCode: z.union([z.string(), z.number()]),
|
||||||
|
message: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
message: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FreebitOtaActivationRequest = z.infer<typeof freebitOtaActivationRequestSchema>;
|
||||||
|
export type FreebitOtaActivationResponse = z.infer<typeof freebitOtaActivationResponseSchema>;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user