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:
tema 2026-01-30 18:22:00 +09:00
parent 56189a9fe8
commit 1283880f7d
19 changed files with 1332 additions and 246 deletions

View File

@ -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 1 02000002470001 PT0220024700010 PASI 20251229 2025-12-29
2 2 02000002470002 PT0220024700020 PASI 20251229 2025-12-29
3 3 02000002470003 PT0220024700030 PASI 20251229 2025-12-29
4 4 02000002470004 PT0220024700040 PASI 20251229 2025-12-29
5 5 02000002470005 PT0220024700050 PASI 20251229 2025-12-29
6 6 02000002470006 PT0220024700060 PASI 20251229 2025-12-29
7 7 02000002470007 PT0220024700070 PASI 20251229 2025-12-29
8 8 02000002470008 PT0220024700080 PASI 20251229 2025-12-29
9 9 02000002470009 PT0220024700090 PASI 20251229 2025-12-29
10 10 02000002470010 PT0220024700100 PASI 20251229 2025-12-29
11 11 02000002470011 PT0220024700110 PASI 20251229 2025-12-29
12 12 02000002470012 PT0220024700120 PASI 20251229 2025-12-29
13 13 02000002470013 PT0220024700130 PASI 20251229 2025-12-29
14 14 02000002470014 PT0220024700140 PASI 20251229 2025-12-29
15 15 02000002470015 PT0220024700150 PASI 20251229 2025-12-29
16 16 02000002470016 PT0220024700160 PASI 20251229 2025-12-29
17 17 02000002470017 PT0220024700170 PASI 20251229 2025-12-29
18 18 02000002470018 PT0220024700180 PASI 20251229 2025-12-29
19 19 02000002470019 PT0220024700190 PASI 20251229 2025-12-29
20 20 02000002470020 PT0220024700200 PASI 20251229 2025-12-29
21 21 02000002470021 PT0220024700210 PASI 20251229 2025-12-29
22 22 02000002470022 PT0220024700220 PASI 20251229 2025-12-29
23 23 02000002470023 PT0220024700230 PASI 20251229 2025-12-29
24 24 02000002470024 PT0220024700240 PASI 20251229 2025-12-29
25 25 02000002470025 PT0220024700250 PASI 20251229 2025-12-29
26 26 02000002470026 PT0220024700260 PASI 20251229 2025-12-29
27 27 02000002470027 PT0220024700270 PASI 20251229 2025-12-29
28 28 02000002470028 PT0220024700280 PASI 20251229 2025-12-29
29 29 02000002470029 PT0220024700290 PASI 20251229 2025-12-29
30 30 02000002470030 PT0220024700300 PASI 20251229 2025-12-29
31 31 02000002470031 PT0220024700310 PASI 20251229 2025-12-29
32 32 02000002470032 PT0220024700320 PASI 20251229 2025-12-29
33 33 02000002470033 PT0220024700330 PASI 20251229 2025-12-29
34 34 02000002470034 PT0220024700340 PASI 20251229 2025-12-29
35 35 02000002470035 PT0220024700350 PASI 20251229 2025-12-29
36 36 02000002470036 PT0220024700360 PASI 20251229 2025-12-29
37 37 02000002470037 PT0220024700370 PASI 20251229 2025-12-29
38 38 02000002470038 PT0220024700380 PASI 20251229 2025-12-29
39 39 02000002470039 PT0220024700390 PASI 20251229 2025-12-29
40 40 02000002470040 PT0220024700400 PASI 20251229 2025-12-29
41 41 02000002470041 PT0220024700410 PASI 20251229 2025-12-29
42 42 02000002470042 PT0220024700420 PASI 20251229 2025-12-29
43 43 02000002470043 PT0220024700430 PASI 20251229 2025-12-29
44 44 02000002470044 PT0220024700440 PASI 20251229 2025-12-29
45 45 02000002470045 PT0220024700450 PASI 20251229 2025-12-29
46 46 02000002470046 PT0220024700460 PASI 20251229 2025-12-29
47 47 02000002470047 PT0220024700470 PASI 20251229 2025-12-29
48 48 02000002470048 PT0220024700480 PASI 20251229 2025-12-29
49 49 02000002470049 PT0220024700490 PASI 20251229 2025-12-29
50 50 02000002470050 PT0220024700500 PASI 20251229 2025-12-29

View File

@ -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

1 51 07000240001 7000240001 PT0270002400010 PASI 2025-12-29 12/29/2025
2 52 07000240002 7000240002 PT0270002400020 PASI 2025-12-29 12/29/2025
3 53 07000240003 7000240003 PT0270002400030 PASI 2025-12-29 12/29/2025
4 54 07000240004 7000240004 PT0270002400040 PASI 2025-12-29 12/29/2025
5 55 07000240005 7000240005 PT0270002400050 PASI 2025-12-29 12/29/2025
6 56 07000240006 7000240006 PT0270002400060 PASI 2025-12-29 12/29/2025
7 57 07000240007 7000240007 PT0270002400070 PASI 2025-12-29 12/29/2025
8 58 07000240008 7000240008 PT0270002400080 PASI 2025-12-29 12/29/2025
9 59 07000240009 7000240009 PT0270002400090 PASI 2025-12-29 12/29/2025
10 60 07000240010 7000240010 PT0270002400100 PASI 2025-12-29 12/29/2025
11 61 07000240011 7000240011 PT0270002400110 PASI 2025-12-29 12/29/2025
12 62 07000240012 7000240012 PT0270002400120 PASI 2025-12-29 12/29/2025
13 63 07000240013 7000240013 PT0270002400130 PASI 2025-12-29 12/29/2025
14 64 07000240014 7000240014 PT0270002400140 PASI 2025-12-29 12/29/2025
15 65 07000240015 7000240015 PT0270002400150 PASI 2025-12-29 12/29/2025
16 66 07000240016 7000240016 PT0270002400160 PASI 2025-12-29 12/29/2025
17 67 07000240017 7000240017 PT0270002400170 PASI 2025-12-29 12/29/2025
18 68 07000240018 7000240018 PT0270002400180 PASI 2025-12-29 12/29/2025
19 69 07000240019 7000240019 PT0270002400190 PASI 2025-12-29 12/29/2025
20 70 07000240020 7000240020 PT0270002400200 PASI 2025-12-29 12/29/2025
21 71 07000240021 7000240021 PT0270002400210 PASI 2025-12-29 12/29/2025
22 72 07000240022 7000240022 PT0270002400220 PASI 2025-12-29 12/29/2025
23 73 07000240023 7000240023 PT0270002400230 PASI 2025-12-29 12/29/2025
24 74 07000240024 7000240024 PT0270002400240 PASI 2025-12-29 12/29/2025
25 75 07000240025 7000240025 PT0270002400250 PASI 2025-12-29 12/29/2025
26 76 07000240026 7000240026 PT0270002400260 PASI 2025-12-29 12/29/2025
27 77 07000240027 7000240027 PT0270002400270 PASI 2025-12-29 12/29/2025
28 78 07000240028 7000240028 PT0270002400280 PASI 2025-12-29 12/29/2025
29 79 07000240029 7000240029 PT0270002400290 PASI 2025-12-29 12/29/2025
30 80 07000240030 7000240030 PT0270002400300 PASI 2025-12-29 12/29/2025
31 81 07000240031 7000240031 PT0270002400310 PASI 2025-12-29 12/29/2025
32 82 07000240032 7000240032 PT0270002400320 PASI 2025-12-29 12/29/2025
33 83 07000240033 7000240033 PT0270002400330 PASI 2025-12-29 12/29/2025
34 84 07000240034 7000240034 PT0270002400340 PASI 2025-12-29 12/29/2025
35 85 07000240035 7000240035 PT0270002400350 PASI 2025-12-29 12/29/2025
36 86 07000240036 7000240036 PT0270002400360 PASI 2025-12-29 12/29/2025
37 87 07000240037 7000240037 PT0270002400370 PASI 2025-12-29 12/29/2025
38 88 07000240038 7000240038 PT0270002400380 PASI 2025-12-29 12/29/2025
39 89 07000240039 7000240039 PT0270002400390 PASI 2025-12-29 12/29/2025
40 90 07000240040 7000240040 PT0270002400400 PASI 2025-12-29 12/29/2025
41 91 07000240041 7000240041 PT0270002400410 PASI 2025-12-29 12/29/2025
42 92 07000240042 7000240042 PT0270002400420 PASI 2025-12-29 12/29/2025
43 93 07000240043 7000240043 PT0270002400430 PASI 2025-12-29 12/29/2025
44 94 07000240044 7000240044 PT0270002400440 PASI 2025-12-29 12/29/2025
45 95 07000240045 7000240045 PT0270002400450 PASI 2025-12-29 12/29/2025
46 96 07000240046 7000240046 PT0270002400460 PASI 2025-12-29 12/29/2025
47 97 07000240047 7000240047 PT0270002400470 PASI 2025-12-29 12/29/2025
48 98 07000240048 7000240048 PT0270002400480 PASI 2025-12-29 12/29/2025
49 99 07000240049 7000240049 PT0270002400490 PASI 2025-12-29 12/29/2025
50 100 07000240050 7000240050 PT0270002400500 PASI 2025-12-29 12/29/2025

View File

@ -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

1 Timestamp API Endpoint API Method Phone Number SIM Identifier Request Payload Response Status Error Additional Info
6 2026-01-29T05:54:55.030Z /mvno/getTrafficInfo/ POST 07000240050 07000240050 {"account":"07000240050"} Error: 210 API Error: NG API Error: NG
7 2026-01-29T05:54:59.051Z /mvno/getTrafficInfo/ POST 07000240050 07000240050 {"account":"07000240050"} Error: 210 API Error: NG API Error: NG
8 2026-01-29T05:55:03.587Z /mvno/getTrafficInfo/ POST 07000240050 07000240050 {"account":"07000240050"} Error: 210 API Error: NG API Error: NG
9 2026-01-30T05:00:27.476Z /mvno/getTrafficInfo/ POST 07000240050 07000240050 {"account":"07000240050"} Error: 210 API Error: NG API Error: NG
10 2026-01-30T05:00:30.722Z /mvno/getTrafficInfo/ POST 07000240050 07000240050 {"account":"07000240050"} Error: 210 API Error: NG API Error: NG
11 2026-01-30T05:00:41.667Z /mvno/getTrafficInfo/ POST 2877252932 2877252932 {"account":"2877252932"} Error: 210 API Error: NG API Error: NG
12 2026-01-30T05:00:41.940Z /mvno/getTrafficInfo/ POST 2877252932 2877252932 {"account":"2877252932"} Error: 210 API Error: NG API Error: NG
13 2026-01-30T05:00:43.417Z /mvno/getTrafficInfo/ POST 2877252932 2877252932 {"account":"2877252932"} Error: 210 API Error: NG API Error: NG
14 2026-01-30T05:00:46.655Z /mvno/getTrafficInfo/ POST 2877252932 2877252932 {"account":"2877252932"} Error: 210 API Error: NG API Error: NG
15 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
16 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
17 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
18 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
19 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
20 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

View File

@ -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 {}

View File

@ -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}`);
}
}
}

View File

@ -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}`;
}
}

View File

@ -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
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────

View File

@ -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,
], ],

View File

@ -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;
} }

View File

@ -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,
};
}
}

View File

@ -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 });

View File

@ -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> {

View File

@ -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;

View File

@ -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: {

View File

@ -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)

View File

@ -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;
}

View File

@ -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";

View File

@ -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>;

View File

@ -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>;