Enhance Freebit Integration and Update UI Components
- Added FreebitTestTrackerService to FreebitClientService for improved API call tracking. - Updated Freebit module to include the new test tracker service. - Refactored HowItWorksSection and PublicInternetPlans to enhance user guidance with clearer steps and improved styling. - Introduced a new VpnHowItWorksSection to provide users with a simplified setup process for VPN services. - Enhanced PublicOfferingCard and VpnPlanCard components with updated styles and additional feature highlights for better user engagement.
This commit is contained in:
parent
ab3561ba5c
commit
94b341dd93
50
ASI_N6_PASI_20251229.csv
Normal file
50
ASI_N6_PASI_20251229.csv
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
1,02000002470001,PT0220024700010,PASI,20251229,,,,
|
||||||
|
2,02000002470002,PT0220024700020,PASI,20251229,,,,
|
||||||
|
3,02000002470003,PT0220024700030,PASI,20251229,,,,
|
||||||
|
4,02000002470004,PT0220024700040,PASI,20251229,,,,
|
||||||
|
5,02000002470005,PT0220024700050,PASI,20251229,,,,
|
||||||
|
6,02000002470006,PT0220024700060,PASI,20251229,,,,
|
||||||
|
7,02000002470007,PT0220024700070,PASI,20251229,,,,
|
||||||
|
8,02000002470008,PT0220024700080,PASI,20251229,,,,
|
||||||
|
9,02000002470009,PT0220024700090,PASI,20251229,,,,
|
||||||
|
10,02000002470010,PT0220024700100,PASI,20251229,,,,
|
||||||
|
11,02000002470011,PT0220024700110,PASI,20251229,,,,
|
||||||
|
12,02000002470012,PT0220024700120,PASI,20251229,,,,
|
||||||
|
13,02000002470013,PT0220024700130,PASI,20251229,,,,
|
||||||
|
14,02000002470014,PT0220024700140,PASI,20251229,,,,
|
||||||
|
15,02000002470015,PT0220024700150,PASI,20251229,,,,
|
||||||
|
16,02000002470016,PT0220024700160,PASI,20251229,,,,
|
||||||
|
17,02000002470017,PT0220024700170,PASI,20251229,,,,
|
||||||
|
18,02000002470018,PT0220024700180,PASI,20251229,,,,
|
||||||
|
19,02000002470019,PT0220024700190,PASI,20251229,,,,
|
||||||
|
20,02000002470020,PT0220024700200,PASI,20251229,,,,
|
||||||
|
21,02000002470021,PT0220024700210,PASI,20251229,,,,
|
||||||
|
22,02000002470022,PT0220024700220,PASI,20251229,,,,
|
||||||
|
23,02000002470023,PT0220024700230,PASI,20251229,,,,
|
||||||
|
24,02000002470024,PT0220024700240,PASI,20251229,,,,
|
||||||
|
25,02000002470025,PT0220024700250,PASI,20251229,,,,
|
||||||
|
26,02000002470026,PT0220024700260,PASI,20251229,,,,
|
||||||
|
27,02000002470027,PT0220024700270,PASI,20251229,,,,
|
||||||
|
28,02000002470028,PT0220024700280,PASI,20251229,,,,
|
||||||
|
29,02000002470029,PT0220024700290,PASI,20251229,,,,
|
||||||
|
30,02000002470030,PT0220024700300,PASI,20251229,,,,
|
||||||
|
31,02000002470031,PT0220024700310,PASI,20251229,,,,
|
||||||
|
32,02000002470032,PT0220024700320,PASI,20251229,,,,
|
||||||
|
33,02000002470033,PT0220024700330,PASI,20251229,,,,
|
||||||
|
34,02000002470034,PT0220024700340,PASI,20251229,,,,
|
||||||
|
35,02000002470035,PT0220024700350,PASI,20251229,,,,
|
||||||
|
36,02000002470036,PT0220024700360,PASI,20251229,,,,
|
||||||
|
37,02000002470037,PT0220024700370,PASI,20251229,,,,
|
||||||
|
38,02000002470038,PT0220024700380,PASI,20251229,,,,
|
||||||
|
39,02000002470039,PT0220024700390,PASI,20251229,,,,
|
||||||
|
40,02000002470040,PT0220024700400,PASI,20251229,,,,
|
||||||
|
41,02000002470041,PT0220024700410,PASI,20251229,,,,
|
||||||
|
42,02000002470042,PT0220024700420,PASI,20251229,,,,
|
||||||
|
43,02000002470043,PT0220024700430,PASI,20251229,,,,
|
||||||
|
44,02000002470044,PT0220024700440,PASI,20251229,,,,
|
||||||
|
45,02000002470045,PT0220024700450,PASI,20251229,,,,
|
||||||
|
46,02000002470046,PT0220024700460,PASI,20251229,,,,
|
||||||
|
47,02000002470047,PT0220024700470,PASI,20251229,,,,
|
||||||
|
48,02000002470048,PT0220024700480,PASI,20251229,,,,
|
||||||
|
49,02000002470049,PT0220024700490,PASI,20251229,,,,
|
||||||
|
50,02000002470050,PT0220024700500,PASI,20251229,,,,
|
||||||
|
50
ASI_N7_PASI_20251229.csv
Normal file
50
ASI_N7_PASI_20251229.csv
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
51,07000240001,PT0270002400010,PASI,20251229,,,,
|
||||||
|
52,07000240002,PT0270002400020,PASI,20251229,,,,
|
||||||
|
53,07000240003,PT0270002400030,PASI,20251229,,,,
|
||||||
|
54,07000240004,PT0270002400040,PASI,20251229,,,,
|
||||||
|
55,07000240005,PT0270002400050,PASI,20251229,,,,
|
||||||
|
56,07000240006,PT0270002400060,PASI,20251229,,,,
|
||||||
|
57,07000240007,PT0270002400070,PASI,20251229,,,,
|
||||||
|
58,07000240008,PT0270002400080,PASI,20251229,,,,
|
||||||
|
59,07000240009,PT0270002400090,PASI,20251229,,,,
|
||||||
|
60,07000240010,PT0270002400100,PASI,20251229,,,,
|
||||||
|
61,07000240011,PT0270002400110,PASI,20251229,,,,
|
||||||
|
62,07000240012,PT0270002400120,PASI,20251229,,,,
|
||||||
|
63,07000240013,PT0270002400130,PASI,20251229,,,,
|
||||||
|
64,07000240014,PT0270002400140,PASI,20251229,,,,
|
||||||
|
65,07000240015,PT0270002400150,PASI,20251229,,,,
|
||||||
|
66,07000240016,PT0270002400160,PASI,20251229,,,,
|
||||||
|
67,07000240017,PT0270002400170,PASI,20251229,,,,
|
||||||
|
68,07000240018,PT0270002400180,PASI,20251229,,,,
|
||||||
|
69,07000240019,PT0270002400190,PASI,20251229,,,,
|
||||||
|
70,07000240020,PT0270002400200,PASI,20251229,,,,
|
||||||
|
71,07000240021,PT0270002400210,PASI,20251229,,,,
|
||||||
|
72,07000240022,PT0270002400220,PASI,20251229,,,,
|
||||||
|
73,07000240023,PT0270002400230,PASI,20251229,,,,
|
||||||
|
74,07000240024,PT0270002400240,PASI,20251229,,,,
|
||||||
|
75,07000240025,PT0270002400250,PASI,20251229,,,,
|
||||||
|
76,07000240026,PT0270002400260,PASI,20251229,,,,
|
||||||
|
77,07000240027,PT0270002400270,PASI,20251229,,,,
|
||||||
|
78,07000240028,PT0270002400280,PASI,20251229,,,,
|
||||||
|
79,07000240029,PT0270002400290,PASI,20251229,,,,
|
||||||
|
80,07000240030,PT0270002400300,PASI,20251229,,,,
|
||||||
|
81,07000240031,PT0270002400310,PASI,20251229,,,,
|
||||||
|
82,07000240032,PT0270002400320,PASI,20251229,,,,
|
||||||
|
83,07000240033,PT0270002400330,PASI,20251229,,,,
|
||||||
|
84,07000240034,PT0270002400340,PASI,20251229,,,,
|
||||||
|
85,07000240035,PT0270002400350,PASI,20251229,,,,
|
||||||
|
86,07000240036,PT0270002400360,PASI,20251229,,,,
|
||||||
|
87,07000240037,PT0270002400370,PASI,20251229,,,,
|
||||||
|
88,07000240038,PT0270002400380,PASI,20251229,,,,
|
||||||
|
89,07000240039,PT0270002400390,PASI,20251229,,,,
|
||||||
|
90,07000240040,PT0270002400400,PASI,20251229,,,,
|
||||||
|
91,07000240041,PT0270002400410,PASI,20251229,,,,
|
||||||
|
92,07000240042,PT0270002400420,PASI,20251229,,,,
|
||||||
|
93,07000240043,PT0270002400430,PASI,20251229,,,,
|
||||||
|
94,07000240044,PT0270002400440,PASI,20251229,,,,
|
||||||
|
95,07000240045,PT0270002400450,PASI,20251229,,,,
|
||||||
|
96,07000240046,PT0270002400460,PASI,20251229,,,,
|
||||||
|
97,07000240047,PT0270002400470,PASI,20251229,,,,
|
||||||
|
98,07000240048,PT0270002400480,PASI,20251229,,,,
|
||||||
|
99,07000240049,PT0270002400490,PASI,20251229,,,,
|
||||||
|
100,07000240050,PT0270002400500,PASI,20251229,,,,
|
||||||
|
1
apps/bff/sim-api-test-log.csv
Normal file
1
apps/bff/sim-api-test-log.csv
Normal file
@ -0,0 +1 @@
|
|||||||
|
Timestamp,API Endpoint,API Method,Phone Number,SIM Identifier,Request Payload,Response Status,Error,Additional Info
|
||||||
|
@ -11,6 +11,7 @@ import { FreebitPlanService } from "./services/freebit-plan.service.js";
|
|||||||
import { FreebitVoiceService } from "./services/freebit-voice.service.js";
|
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 { SimManagementModule } from "../../modules/subscriptions/sim-management/sim-management.module.js";
|
import { SimManagementModule } from "../../modules/subscriptions/sim-management/sim-management.module.js";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -21,6 +22,7 @@ import { SimManagementModule } from "../../modules/subscriptions/sim-management/
|
|||||||
FreebitAuthService,
|
FreebitAuthService,
|
||||||
FreebitMapperService,
|
FreebitMapperService,
|
||||||
FreebitRateLimiterService,
|
FreebitRateLimiterService,
|
||||||
|
FreebitTestTrackerService,
|
||||||
// Specialized operation services
|
// Specialized operation services
|
||||||
FreebitAccountService,
|
FreebitAccountService,
|
||||||
FreebitUsageService,
|
FreebitUsageService,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { RetryableErrors, withRetry } from "@bff/core/utils/retry.util.js";
|
|||||||
import { redactForLogs } from "@bff/core/logging/redaction.util.js";
|
import { redactForLogs } from "@bff/core/logging/redaction.util.js";
|
||||||
import { FreebitAuthService } from "./freebit-auth.service.js";
|
import { FreebitAuthService } from "./freebit-auth.service.js";
|
||||||
import { FreebitError } from "./freebit-error.service.js";
|
import { FreebitError } from "./freebit-error.service.js";
|
||||||
|
import { FreebitTestTrackerService } from "./freebit-test-tracker.service.js";
|
||||||
|
|
||||||
interface FreebitResponseBase {
|
interface FreebitResponseBase {
|
||||||
resultCode?: string | number;
|
resultCode?: string | number;
|
||||||
@ -18,6 +19,7 @@ interface FreebitResponseBase {
|
|||||||
export class FreebitClientService {
|
export class FreebitClientService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly authService: FreebitAuthService,
|
private readonly authService: FreebitAuthService,
|
||||||
|
private readonly testTracker: FreebitTestTrackerService,
|
||||||
@Inject(Logger) private readonly logger: Logger
|
@Inject(Logger) private readonly logger: Logger
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -35,7 +37,8 @@ export class FreebitClientService {
|
|||||||
const requestPayload = { ...payload, authKey };
|
const requestPayload = { ...payload, authKey };
|
||||||
|
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
return withRetry(
|
try {
|
||||||
|
const responseData = await withRetry(
|
||||||
async () => {
|
async () => {
|
||||||
attempt += 1;
|
attempt += 1;
|
||||||
|
|
||||||
@ -108,7 +111,9 @@ export class FreebitClientService {
|
|||||||
isRetryable: error => {
|
isRetryable: error => {
|
||||||
if (error instanceof FreebitError) {
|
if (error instanceof FreebitError) {
|
||||||
if (error.isAuthError() && attempt === 1) {
|
if (error.isAuthError() && attempt === 1) {
|
||||||
this.logger.warn("Freebit auth error detected, clearing cache and retrying", { url });
|
this.logger.warn("Freebit auth error detected, clearing cache and retrying", {
|
||||||
|
url,
|
||||||
|
});
|
||||||
this.authService.clearAuthCache();
|
this.authService.clearAuthCache();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -120,6 +125,24 @@ export class FreebitClientService {
|
|||||||
logContext: "Freebit API request",
|
logContext: "Freebit API request",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Track successful API call
|
||||||
|
this.trackApiCall(endpoint, payload, responseData, null).catch((error: unknown) => {
|
||||||
|
this.logger.debug("Failed to track API call", {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return responseData;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// Track failed API call
|
||||||
|
this.trackApiCall(endpoint, payload, null, error).catch((trackError: unknown) => {
|
||||||
|
this.logger.debug("Failed to track API call error", {
|
||||||
|
error: trackError instanceof Error ? trackError.message : String(trackError),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,7 +156,8 @@ export class FreebitClientService {
|
|||||||
const url = this.buildUrl(config.baseUrl, endpoint);
|
const url = this.buildUrl(config.baseUrl, endpoint);
|
||||||
|
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
return withRetry(
|
try {
|
||||||
|
const responseData = await withRetry(
|
||||||
async () => {
|
async () => {
|
||||||
attempt += 1;
|
attempt += 1;
|
||||||
this.logger.debug("Freebit JSON API request", {
|
this.logger.debug("Freebit JSON API request", {
|
||||||
@ -196,7 +220,9 @@ export class FreebitClientService {
|
|||||||
isRetryable: error => {
|
isRetryable: error => {
|
||||||
if (error instanceof FreebitError) {
|
if (error instanceof FreebitError) {
|
||||||
if (error.isAuthError() && attempt === 1) {
|
if (error.isAuthError() && attempt === 1) {
|
||||||
this.logger.warn("Freebit auth error detected, clearing cache and retrying", { url });
|
this.logger.warn("Freebit auth error detected, clearing cache and retrying", {
|
||||||
|
url,
|
||||||
|
});
|
||||||
this.authService.clearAuthCache();
|
this.authService.clearAuthCache();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -208,6 +234,24 @@ export class FreebitClientService {
|
|||||||
logContext: "Freebit JSON API request",
|
logContext: "Freebit JSON API request",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Track successful API call
|
||||||
|
this.trackApiCall(endpoint, payload, responseData, null).catch((error: unknown) => {
|
||||||
|
this.logger.debug("Failed to track API call", {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return responseData;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// Track failed API call
|
||||||
|
this.trackApiCall(endpoint, payload, null, error).catch((trackError: unknown) => {
|
||||||
|
this.logger.debug("Failed to track API call error", {
|
||||||
|
error: trackError instanceof Error ? trackError.message : String(trackError),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -264,4 +308,52 @@ export class FreebitClientService {
|
|||||||
const normalized = String(code).trim();
|
const normalized = String(code).trim();
|
||||||
return normalized.length > 0 ? normalized : undefined;
|
return normalized.length > 0 ? normalized : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track API call for testing purposes
|
||||||
|
*/
|
||||||
|
private async trackApiCall(
|
||||||
|
endpoint: string,
|
||||||
|
payload: unknown,
|
||||||
|
response: FreebitResponseBase | null,
|
||||||
|
error: unknown
|
||||||
|
): Promise<void> {
|
||||||
|
const payloadObj = payload as Record<string, unknown>;
|
||||||
|
const phoneNumber = this.testTracker.extractPhoneNumber(
|
||||||
|
(payloadObj.account as string) || "",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only track if we have a phone number (SIM-related calls)
|
||||||
|
if (!phoneNumber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = this.testTracker.getCurrentTimestamp();
|
||||||
|
const resultCode = response?.resultCode
|
||||||
|
? String(response.resultCode)
|
||||||
|
: error instanceof FreebitError
|
||||||
|
? String(error.resultCode || "ERROR")
|
||||||
|
: "ERROR";
|
||||||
|
|
||||||
|
const statusMessage =
|
||||||
|
response?.status?.message ||
|
||||||
|
(error instanceof FreebitError
|
||||||
|
? error.message
|
||||||
|
: error
|
||||||
|
? extractErrorMessage(error)
|
||||||
|
: "Success");
|
||||||
|
|
||||||
|
await this.testTracker.logApiCall({
|
||||||
|
timestamp,
|
||||||
|
apiEndpoint: endpoint,
|
||||||
|
apiMethod: "POST",
|
||||||
|
phoneNumber,
|
||||||
|
simIdentifier: (payloadObj.account as string) || phoneNumber,
|
||||||
|
requestPayload: JSON.stringify(redactForLogs(payload)),
|
||||||
|
responseStatus: resultCode === "100" ? "Success" : `Error: ${resultCode}`,
|
||||||
|
error: error ? extractErrorMessage(error) : undefined,
|
||||||
|
additionalInfo: statusMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,137 @@
|
|||||||
|
import { Injectable, Inject } from "@nestjs/common";
|
||||||
|
import { Logger } from "nestjs-pino";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
export interface TestTrackingRecord {
|
||||||
|
timestamp: string;
|
||||||
|
apiEndpoint: string;
|
||||||
|
apiMethod: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
simIdentifier?: string;
|
||||||
|
requestPayload?: string;
|
||||||
|
responseStatus?: string;
|
||||||
|
error?: string;
|
||||||
|
additionalInfo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for tracking physical SIM test API calls
|
||||||
|
* Logs all API calls to a CSV file for testing purposes
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class FreebitTestTrackerService {
|
||||||
|
private readonly logFilePath: string;
|
||||||
|
private readonly csvHeaders = [
|
||||||
|
"Timestamp",
|
||||||
|
"API Endpoint",
|
||||||
|
"API Method",
|
||||||
|
"Phone Number",
|
||||||
|
"SIM Identifier",
|
||||||
|
"Request Payload",
|
||||||
|
"Response Status",
|
||||||
|
"Error",
|
||||||
|
"Additional Info",
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(@Inject(Logger) private readonly logger: Logger) {
|
||||||
|
// Store log file in project root
|
||||||
|
const projectRoot = process.cwd();
|
||||||
|
this.logFilePath = join(projectRoot, "sim-api-test-log.csv");
|
||||||
|
|
||||||
|
// Initialize CSV file with headers if it doesn't exist
|
||||||
|
this.initializeLogFile().catch((error: unknown) => {
|
||||||
|
this.logger.error("Failed to initialize test tracking log file", {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the CSV log file with headers if it doesn't exist
|
||||||
|
*/
|
||||||
|
private async initializeLogFile(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.access(this.logFilePath);
|
||||||
|
// File exists, no need to write headers
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist, create it with headers
|
||||||
|
const headers = this.csvHeaders.join(",") + "\n";
|
||||||
|
await fs.writeFile(this.logFilePath, headers, "utf-8");
|
||||||
|
this.logger.log("Created test tracking log file", { path: this.logFilePath });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an API call to the CSV file
|
||||||
|
*/
|
||||||
|
async logApiCall(record: TestTrackingRecord): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.initializeLogFile();
|
||||||
|
|
||||||
|
// Format the record as CSV row
|
||||||
|
const row =
|
||||||
|
[
|
||||||
|
record.timestamp,
|
||||||
|
this.escapeCsvField(record.apiEndpoint),
|
||||||
|
this.escapeCsvField(record.apiMethod),
|
||||||
|
this.escapeCsvField(record.phoneNumber),
|
||||||
|
this.escapeCsvField(record.simIdentifier || ""),
|
||||||
|
this.escapeCsvField(record.requestPayload || ""),
|
||||||
|
this.escapeCsvField(record.responseStatus || ""),
|
||||||
|
this.escapeCsvField(record.error || ""),
|
||||||
|
this.escapeCsvField(record.additionalInfo || ""),
|
||||||
|
].join(",") + "\n";
|
||||||
|
|
||||||
|
// Append to file
|
||||||
|
await fs.appendFile(this.logFilePath, row, "utf-8");
|
||||||
|
|
||||||
|
this.logger.debug("Logged API call to test tracking file", {
|
||||||
|
endpoint: record.apiEndpoint,
|
||||||
|
phoneNumber: record.phoneNumber,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Don't throw - logging failures shouldn't break the API
|
||||||
|
this.logger.error("Failed to log API call to test tracking file", {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
record,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape CSV field values (handle commas, quotes, newlines)
|
||||||
|
*/
|
||||||
|
private escapeCsvField(field: string): string {
|
||||||
|
if (!field) return "";
|
||||||
|
|
||||||
|
// If field contains comma, quote, or newline, wrap in quotes and escape quotes
|
||||||
|
if (field.includes(",") || field.includes('"') || field.includes("\n")) {
|
||||||
|
return `"${field.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract phone number from account or payload
|
||||||
|
*/
|
||||||
|
extractPhoneNumber(account: string, payload?: unknown): string {
|
||||||
|
if (account) return account;
|
||||||
|
|
||||||
|
// Try to extract from payload if it's an object
|
||||||
|
if (payload && typeof payload === "object") {
|
||||||
|
const obj = payload as Record<string, unknown>;
|
||||||
|
return (obj.account as string) || (obj.msisdn as string) || (obj.phoneNumber as string) || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format timestamp as ISO string
|
||||||
|
*/
|
||||||
|
getCurrentTimestamp(): string {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,44 +1,31 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { MapPin, Settings, Calendar, Wifi } from "lucide-react";
|
||||||
UserPlusIcon,
|
|
||||||
MagnifyingGlassIcon,
|
|
||||||
CheckBadgeIcon,
|
|
||||||
RocketLaunchIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
interface StepProps {
|
interface StepProps {
|
||||||
number: number;
|
number: number;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
isLast?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Step({ number, icon, title, description, isLast = false }: StepProps) {
|
function Step({ number, icon, title, description }: StepProps) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-start gap-4">
|
<div className="flex flex-col items-center text-center flex-1 min-w-0">
|
||||||
{/* Step number with icon */}
|
{/* Icon with number badge */}
|
||||||
<div className="flex flex-col items-center">
|
<div className="relative mb-4">
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 border-2 border-primary/30 text-primary">
|
<div className="flex h-16 w-16 items-center justify-center rounded-xl bg-gray-50 border border-gray-200 text-primary shadow-sm">
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
{/* Connector line */}
|
{/* Number badge */}
|
||||||
{!isLast && (
|
<div className="absolute -top-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-primary text-white text-xs font-bold shadow-sm">
|
||||||
<div className="w-0.5 h-full min-h-[3rem] bg-gradient-to-b from-primary/30 to-transparent mt-2" />
|
{number}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 pb-8">
|
<h4 className="font-semibold text-foreground mb-2">{title}</h4>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<p className="text-sm text-muted-foreground leading-relaxed max-w-[180px]">{description}</p>
|
||||||
<span className="text-xs font-bold text-primary uppercase tracking-wider">
|
|
||||||
Step {number}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h4 className="font-semibold text-foreground mb-1">{title}</h4>
|
|
||||||
<p className="text-sm text-muted-foreground leading-relaxed">{description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -46,41 +33,58 @@ function Step({ number, icon, title, description, isLast = false }: StepProps) {
|
|||||||
export function HowItWorksSection() {
|
export function HowItWorksSection() {
|
||||||
const steps = [
|
const steps = [
|
||||||
{
|
{
|
||||||
icon: <UserPlusIcon className="h-5 w-5" />,
|
icon: <MapPin className="h-6 w-6" />,
|
||||||
title: "Create your account",
|
title: "Enter Address",
|
||||||
description:
|
description: "Submit your address for coverage check",
|
||||||
"Sign up with your email and provide your service address. This only takes a minute.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <MagnifyingGlassIcon className="h-5 w-5" />,
|
icon: <Settings className="h-6 w-6" />,
|
||||||
title: "We verify with NTT",
|
title: "We Verify",
|
||||||
description:
|
description: "Our team checks with NTT (1-2 days)",
|
||||||
"Our team checks what service is available at your address. This takes 1-2 business days.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <CheckBadgeIcon className="h-5 w-5" />,
|
icon: <Calendar className="h-6 w-6" />,
|
||||||
title: "Choose your plan",
|
title: "Sign Up & Order",
|
||||||
description:
|
description: "Create account and select your plan",
|
||||||
"Once verified, you'll see exactly which plans are available and can select your tier (Silver, Gold, or Platinum).",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <RocketLaunchIcon className="h-5 w-5" />,
|
icon: <Wifi className="h-6 w-6" />,
|
||||||
title: "Get connected",
|
title: "Get Connected",
|
||||||
description:
|
description: "NTT installs fiber at your home",
|
||||||
"We coordinate NTT installation and set up your service. You'll be online in no time.",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] p-6">
|
<section className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] p-8">
|
||||||
<div className="mb-6">
|
{/* Header */}
|
||||||
<h3 className="text-lg font-bold text-foreground mb-1">How it works</h3>
|
<div className="text-center mb-8">
|
||||||
<p className="text-sm text-muted-foreground">
|
<span className="text-sm font-semibold text-primary uppercase tracking-wider">
|
||||||
Getting connected is simple. Here's what to expect.
|
Getting Started
|
||||||
</p>
|
</span>
|
||||||
|
<h3 className="text-2xl font-bold text-foreground mt-1">How It Works</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-0">
|
{/* Steps with connecting line */}
|
||||||
|
<div className="relative">
|
||||||
|
{/* Connecting line - hidden on mobile */}
|
||||||
|
<div className="hidden md:block absolute top-8 left-[12%] right-[12%] h-0.5 bg-gray-200" />
|
||||||
|
|
||||||
|
{/* Curved path SVG for visual connection - hidden on mobile */}
|
||||||
|
<svg
|
||||||
|
className="hidden md:block absolute top-[30px] left-0 right-0 w-full h-4 pointer-events-none"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M 12% 8 Q 30% 8, 37.5% 8 Q 45% 8, 50% 8 Q 55% 8, 62.5% 8 Q 70% 8, 88% 8"
|
||||||
|
fill="none"
|
||||||
|
stroke="#e5e7eb"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeDasharray="6 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Steps grid */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8 relative z-10">
|
||||||
{steps.map((step, index) => (
|
{steps.map((step, index) => (
|
||||||
<Step
|
<Step
|
||||||
key={index}
|
key={index}
|
||||||
@ -88,10 +92,10 @@ export function HowItWorksSection() {
|
|||||||
icon={step.icon}
|
icon={step.icon}
|
||||||
title={step.title}
|
title={step.title}
|
||||||
description={step.description}
|
description={step.description}
|
||||||
isLast={index === steps.length - 1}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ChevronDown, ChevronUp, Home, Building2, Zap, Info, X } from "lucide-react";
|
import { ChevronDown, ChevronUp, Home, Building2, Info, X, Sparkles } from "lucide-react";
|
||||||
import { Button } from "@/components/atoms/button";
|
import { Button } from "@/components/atoms/button";
|
||||||
import { CardBadge } from "@/features/services/components/base/CardBadge";
|
import { CardBadge } from "@/features/services/components/base/CardBadge";
|
||||||
import { cn } from "@/shared/utils";
|
import { cn } from "@/shared/utils";
|
||||||
@ -38,15 +38,15 @@ interface PublicOfferingCardProps {
|
|||||||
|
|
||||||
const tierStyles = {
|
const tierStyles = {
|
||||||
Silver: {
|
Silver: {
|
||||||
card: "border-muted-foreground/20 bg-card",
|
card: "border-gray-200 bg-white border-l-4 border-l-gray-400",
|
||||||
accent: "text-muted-foreground",
|
accent: "text-gray-600",
|
||||||
},
|
},
|
||||||
Gold: {
|
Gold: {
|
||||||
card: "border-warning/30 bg-warning-soft/20",
|
card: "border-gray-200 bg-white border-l-4 border-l-amber-500",
|
||||||
accent: "text-warning",
|
accent: "text-amber-600",
|
||||||
},
|
},
|
||||||
Platinum: {
|
Platinum: {
|
||||||
card: "border-primary/30 bg-info-soft/20",
|
card: "border-gray-200 bg-white border-l-4 border-l-primary",
|
||||||
accent: "text-primary",
|
accent: "text-primary",
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
@ -213,12 +213,24 @@ export function PublicOfferingCard({
|
|||||||
<div
|
<div
|
||||||
key={tier.tier}
|
key={tier.tier}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-lg border p-3 transition-all duration-200 flex flex-col",
|
"rounded-lg border p-3 transition-all duration-200 flex flex-col relative",
|
||||||
tierStyles[tier.tier].card
|
tierStyles[tier.tier].card
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{/* Popular Badge for Gold */}
|
||||||
|
{tier.tier === "Gold" && (
|
||||||
|
<div className="absolute -top-2.5 left-1/2 -translate-x-1/2">
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-500 text-white text-[10px] font-semibold shadow-sm">
|
||||||
|
<Sparkles className="h-2.5 w-2.5" />
|
||||||
|
Popular
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div
|
||||||
|
className={cn("flex items-center gap-2 mb-2", tier.tier === "Gold" ? "mt-1" : "")}
|
||||||
|
>
|
||||||
<span className={cn("font-semibold text-sm", tierStyles[tier.tier].accent)}>
|
<span className={cn("font-semibold text-sm", tierStyles[tier.tier].accent)}>
|
||||||
{tier.tier}
|
{tier.tier}
|
||||||
</span>
|
</span>
|
||||||
@ -235,7 +247,11 @@ export function PublicOfferingCard({
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">/mo</span>
|
<span className="text-xs text-muted-foreground">/mo</span>
|
||||||
{tier.pricingNote && (
|
{tier.pricingNote && (
|
||||||
<span className="text-[10px] text-warning ml-1">{tier.pricingNote}</span>
|
<span
|
||||||
|
className={`text-[10px] ml-1 ${tier.tier === "Platinum" ? "text-primary" : "text-amber-600"}`}
|
||||||
|
>
|
||||||
|
{tier.pricingNote}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -247,7 +263,17 @@ export function PublicOfferingCard({
|
|||||||
<ul className="space-y-1 flex-grow">
|
<ul className="space-y-1 flex-grow">
|
||||||
{tier.features.slice(0, 3).map((feature, index) => (
|
{tier.features.slice(0, 3).map((feature, index) => (
|
||||||
<li key={index} className="flex items-start gap-1.5 text-xs">
|
<li key={index} className="flex items-start gap-1.5 text-xs">
|
||||||
<Zap className="h-3 w-3 text-success flex-shrink-0 mt-0.5" />
|
<svg
|
||||||
|
className="h-3 w-3 text-gray-500 flex-shrink-0 mt-0.5"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
<span className="text-muted-foreground leading-relaxed">{feature}</span>
|
<span className="text-muted-foreground leading-relaxed">{feature}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -0,0 +1,101 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Signal, FileCheck, Send, CheckCircle } from "lucide-react";
|
||||||
|
|
||||||
|
interface StepProps {
|
||||||
|
number: number;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Step({ number, icon, title, description }: StepProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center text-center flex-1 min-w-0">
|
||||||
|
{/* Icon with number badge */}
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<div className="flex h-16 w-16 items-center justify-center rounded-xl bg-gray-50 border border-gray-200 text-primary shadow-sm">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
{/* Number badge */}
|
||||||
|
<div className="absolute -top-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-primary text-white text-xs font-bold shadow-sm">
|
||||||
|
{number}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<h4 className="font-semibold text-foreground mb-2">{title}</h4>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed max-w-[180px]">{description}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SimHowItWorksSection() {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
icon: <Signal className="h-6 w-6" />,
|
||||||
|
title: "Choose Plan",
|
||||||
|
description: "Select your data and voice options",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FileCheck className="h-6 w-6" />,
|
||||||
|
title: "Create Account",
|
||||||
|
description: "Sign up with email verification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Send className="h-6 w-6" />,
|
||||||
|
title: "Place Order",
|
||||||
|
description: "Configure SIM type and pay",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <CheckCircle className="h-6 w-6" />,
|
||||||
|
title: "Get Connected",
|
||||||
|
description: "Receive SIM and activate",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] p-8 mb-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<span className="text-sm font-semibold text-primary uppercase tracking-wider">
|
||||||
|
Getting Started
|
||||||
|
</span>
|
||||||
|
<h3 className="text-2xl font-bold text-foreground mt-1">How It Works</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Steps with connecting line */}
|
||||||
|
<div className="relative">
|
||||||
|
{/* Connecting line - hidden on mobile */}
|
||||||
|
<div className="hidden md:block absolute top-8 left-[12%] right-[12%] h-0.5 bg-gray-200" />
|
||||||
|
|
||||||
|
{/* Curved path SVG for visual connection - hidden on mobile */}
|
||||||
|
<svg
|
||||||
|
className="hidden md:block absolute top-[30px] left-0 right-0 w-full h-4 pointer-events-none"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M 12% 8 Q 30% 8, 37.5% 8 Q 45% 8, 50% 8 Q 55% 8, 62.5% 8 Q 70% 8, 88% 8"
|
||||||
|
fill="none"
|
||||||
|
stroke="#e5e7eb"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeDasharray="6 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Steps grid */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8 relative z-10">
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<Step
|
||||||
|
key={index}
|
||||||
|
number={index + 1}
|
||||||
|
icon={step.icon}
|
||||||
|
title={step.title}
|
||||||
|
description={step.description}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -30,6 +30,7 @@ import {
|
|||||||
ServiceHighlights,
|
ServiceHighlights,
|
||||||
type HighlightFeature,
|
type HighlightFeature,
|
||||||
} from "@/features/services/components/base/ServiceHighlights";
|
} from "@/features/services/components/base/ServiceHighlights";
|
||||||
|
import { SimHowItWorksSection } from "@/features/services/components/sim/SimHowItWorksSection";
|
||||||
|
|
||||||
export type SimPlansTab = "data-voice" | "data-only" | "voice-only";
|
export type SimPlansTab = "data-voice" | "data-only" | "voice-only";
|
||||||
|
|
||||||
@ -397,6 +398,11 @@ export function SimPlansContent({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* How It Works Section */}
|
||||||
|
<div className="mt-12">
|
||||||
|
<SimHowItWorksSection />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 space-y-4">
|
<div className="mt-8 space-y-4">
|
||||||
<CollapsibleSection title="Calling & SMS Rates" icon={Phone}>
|
<CollapsibleSection title="Calling & SMS Rates" icon={Phone}>
|
||||||
<div className="space-y-6 pt-4">
|
<div className="space-y-6 pt-4">
|
||||||
|
|||||||
@ -2,31 +2,70 @@
|
|||||||
|
|
||||||
import { AnimatedCard } from "@/components/molecules";
|
import { AnimatedCard } from "@/components/molecules";
|
||||||
import { Button } from "@/components/atoms/button";
|
import { Button } from "@/components/atoms/button";
|
||||||
import { ArrowRight, ShieldCheck } from "lucide-react";
|
import { ArrowRight, Globe, Check } from "lucide-react";
|
||||||
import type { VpnCatalogProduct } from "@customer-portal/domain/services";
|
import type { VpnCatalogProduct } from "@customer-portal/domain/services";
|
||||||
import { CardPricing } from "@/features/services/components/base/CardPricing";
|
|
||||||
|
|
||||||
interface VpnPlanCardProps {
|
interface VpnPlanCardProps {
|
||||||
plan: VpnCatalogProduct;
|
plan: VpnCatalogProduct;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const vpnFeatures = [
|
||||||
|
"Secure VPN connection",
|
||||||
|
"Pre-configured router",
|
||||||
|
"Easy plug & play setup",
|
||||||
|
"English support included",
|
||||||
|
];
|
||||||
|
|
||||||
export function VpnPlanCard({ plan }: VpnPlanCardProps) {
|
export function VpnPlanCard({ plan }: VpnPlanCardProps) {
|
||||||
return (
|
return (
|
||||||
<AnimatedCard className="p-6 border border-primary/20 hover:border-primary/40 transition-all duration-300 hover:shadow-lg flex flex-col h-full">
|
<AnimatedCard className="p-6 border border-border hover:border-primary/40 transition-all duration-300 hover:shadow-lg flex flex-col h-full bg-white">
|
||||||
{/* Header with icon and name */}
|
{/* Header with icon and name */}
|
||||||
<div className="flex items-start gap-3 mb-4">
|
<div className="flex items-start justify-between mb-4">
|
||||||
<div className="p-2 bg-primary/10 rounded-lg">
|
<div className="flex items-start gap-3">
|
||||||
<ShieldCheck className="h-6 w-6 text-primary" />
|
<div className="p-2.5 bg-primary/10 rounded-xl">
|
||||||
|
<Globe className="h-6 w-6 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="text-xl font-semibold text-foreground">{plan.name}</h3>
|
<h3 className="text-lg font-bold text-foreground">{plan.name}</h3>
|
||||||
|
<span className="text-sm text-muted-foreground">International</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-1.5 rounded-full border border-primary/30">
|
||||||
|
<Check className="h-4 w-4 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pricing */}
|
{/* Pricing */}
|
||||||
<div className="mb-6">
|
<div className="mb-4">
|
||||||
<CardPricing monthlyPrice={plan.monthlyPrice} size="lg" alignment="left" />
|
<div className="flex items-baseline gap-1">
|
||||||
|
<span className="text-sm text-primary">¥</span>
|
||||||
|
<span className="text-3xl font-bold text-foreground">
|
||||||
|
{(plan.monthlyPrice ?? 0).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">/month</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">Router rental included</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features list */}
|
||||||
|
<ul className="space-y-2 mb-6 flex-grow">
|
||||||
|
{vpnFeatures.map((feature, index) => (
|
||||||
|
<li key={index} className="flex items-start gap-2 text-sm">
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4 text-gray-500 flex-shrink-0 mt-0.5"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="text-muted-foreground">{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
{/* Action Button */}
|
{/* Action Button */}
|
||||||
<div className="mt-auto">
|
<div className="mt-auto">
|
||||||
@ -36,7 +75,7 @@ export function VpnPlanCard({ plan }: VpnPlanCardProps) {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
rightIcon={<ArrowRight className="w-4 h-4" />}
|
rightIcon={<ArrowRight className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
Continue to Checkout
|
Select {plan.name}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
|
|||||||
@ -28,22 +28,23 @@ import {
|
|||||||
ServiceHighlights,
|
ServiceHighlights,
|
||||||
HighlightFeature,
|
HighlightFeature,
|
||||||
} from "@/features/services/components/base/ServiceHighlights";
|
} from "@/features/services/components/base/ServiceHighlights";
|
||||||
|
import { HowItWorksSection } from "@/features/services/components/internet/HowItWorksSection";
|
||||||
import { cn } from "@/shared/utils";
|
import { cn } from "@/shared/utils";
|
||||||
|
|
||||||
// Tier styling
|
// Tier styling - matching the design with left border accents
|
||||||
const tierStyles = {
|
const tierStyles = {
|
||||||
Silver: {
|
Silver: {
|
||||||
card: "border-muted-foreground/20 bg-card",
|
card: "border-gray-200 bg-white border-l-4 border-l-gray-400",
|
||||||
accent: "text-muted-foreground",
|
accent: "text-gray-600",
|
||||||
header: "Silver",
|
header: "Silver",
|
||||||
},
|
},
|
||||||
Gold: {
|
Gold: {
|
||||||
card: "border-warning/30 bg-warning-soft/20",
|
card: "border-gray-200 bg-white border-l-4 border-l-amber-500",
|
||||||
accent: "text-warning",
|
accent: "text-amber-600",
|
||||||
header: "Gold",
|
header: "Gold",
|
||||||
},
|
},
|
||||||
Platinum: {
|
Platinum: {
|
||||||
card: "border-primary/30 bg-info-soft/20",
|
card: "border-gray-200 bg-white border-l-4 border-l-primary",
|
||||||
accent: "text-primary",
|
accent: "text-primary",
|
||||||
header: "Platinum",
|
header: "Platinum",
|
||||||
},
|
},
|
||||||
@ -110,28 +111,48 @@ function ConsolidatedInternetCard({
|
|||||||
<div
|
<div
|
||||||
key={tier.tier}
|
key={tier.tier}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-xl border p-4 transition-all duration-200 flex flex-col",
|
"rounded-xl border p-4 transition-all duration-200 flex flex-col relative",
|
||||||
tierStyles[tier.tier].card
|
tierStyles[tier.tier].card
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{/* Popular Badge for Gold */}
|
||||||
|
{tier.tier === "Gold" && (
|
||||||
|
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
|
||||||
|
<span className="inline-flex items-center gap-1 px-3 py-1 rounded-full bg-amber-500 text-white text-xs font-semibold shadow-sm">
|
||||||
|
<Sparkles className="h-3 w-3" />
|
||||||
|
Popular
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Tier Name */}
|
{/* Tier Name */}
|
||||||
<h4 className={cn("font-bold text-lg mb-2", tierStyles[tier.tier].accent)}>
|
<h4
|
||||||
|
className={cn(
|
||||||
|
"font-bold text-lg mb-2",
|
||||||
|
tier.tier === "Gold" ? "mt-2" : "",
|
||||||
|
tierStyles[tier.tier].accent
|
||||||
|
)}
|
||||||
|
>
|
||||||
{tier.tier}
|
{tier.tier}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
{/* Price Range */}
|
{/* Price Range */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="flex items-baseline gap-0.5 flex-wrap">
|
<div className="flex items-baseline gap-0.5 flex-wrap">
|
||||||
<span className="text-xl font-bold text-foreground">
|
<span className="text-2xl font-bold text-foreground">
|
||||||
¥{tier.monthlyPrice.toLocaleString()}
|
¥{tier.monthlyPrice.toLocaleString()}
|
||||||
{tier.maxMonthlyPrice &&
|
{tier.maxMonthlyPrice &&
|
||||||
tier.maxMonthlyPrice > tier.monthlyPrice &&
|
tier.maxMonthlyPrice > tier.monthlyPrice &&
|
||||||
`~${tier.maxMonthlyPrice.toLocaleString()}`}
|
`~${tier.maxMonthlyPrice.toLocaleString()}`}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">/mo</span>
|
<span className="text-sm text-muted-foreground">/mo</span>
|
||||||
</div>
|
</div>
|
||||||
{tier.pricingNote && (
|
{tier.pricingNote && (
|
||||||
<span className="text-xs text-warning">{tier.pricingNote}</span>
|
<span
|
||||||
|
className={`text-xs ${tier.tier === "Platinum" ? "text-primary" : "text-amber-600"}`}
|
||||||
|
>
|
||||||
|
{tier.pricingNote}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -142,7 +163,17 @@ function ConsolidatedInternetCard({
|
|||||||
<ul className="space-y-2 flex-grow">
|
<ul className="space-y-2 flex-grow">
|
||||||
{tier.features.map((feature, index) => (
|
{tier.features.map((feature, index) => (
|
||||||
<li key={index} className="flex items-start gap-2 text-sm">
|
<li key={index} className="flex items-start gap-2 text-sm">
|
||||||
<Zap className="h-4 w-4 text-success flex-shrink-0 mt-0.5" />
|
<svg
|
||||||
|
className="h-4 w-4 text-gray-500 flex-shrink-0 mt-0.5"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
<span className="text-muted-foreground">{feature}</span>
|
<span className="text-muted-foreground">{feature}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@ -408,6 +439,9 @@ export function PublicInternetPlansContent({
|
|||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* How It Works Section */}
|
||||||
|
<HowItWorksSection />
|
||||||
|
|
||||||
{/* Final CTA - Polished */}
|
{/* Final CTA - Polished */}
|
||||||
<section className="text-center py-8 mt-4 rounded-2xl bg-gradient-to-br from-primary/5 via-transparent to-info/5 border border-border/50">
|
<section className="text-center py-8 mt-4 rounded-2xl bg-gradient-to-br from-primary/5 via-transparent to-info/5 border border-border/50">
|
||||||
<div className="inline-flex items-center gap-1.5 text-xs font-medium text-primary mb-3 px-3 py-1 rounded-full bg-primary/10">
|
<div className="inline-flex items-center gap-1.5 text-xs font-medium text-primary mb-3 px-3 py-1 rounded-full bg-primary/10">
|
||||||
|
|||||||
@ -1,7 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ShieldCheck, Zap, ChevronDown, ChevronUp } from "lucide-react";
|
import {
|
||||||
|
ShieldCheck,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
Router,
|
||||||
|
Globe,
|
||||||
|
Tv,
|
||||||
|
Wifi,
|
||||||
|
Package,
|
||||||
|
Headphones,
|
||||||
|
CreditCard,
|
||||||
|
Play,
|
||||||
|
} from "lucide-react";
|
||||||
import { usePublicVpnCatalog } from "@/features/services/hooks";
|
import { usePublicVpnCatalog } from "@/features/services/hooks";
|
||||||
import { LoadingCard } from "@/components/atoms";
|
import { LoadingCard } from "@/components/atoms";
|
||||||
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
|
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
|
||||||
@ -10,6 +22,10 @@ import { VpnPlanCard } from "@/features/services/components/vpn/VpnPlanCard";
|
|||||||
import { ServicesBackLink } from "@/features/services/components/base/ServicesBackLink";
|
import { ServicesBackLink } from "@/features/services/components/base/ServicesBackLink";
|
||||||
import { ServicesHero } from "@/features/services/components/base/ServicesHero";
|
import { ServicesHero } from "@/features/services/components/base/ServicesHero";
|
||||||
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
|
||||||
|
import {
|
||||||
|
ServiceHighlights,
|
||||||
|
type HighlightFeature,
|
||||||
|
} from "@/features/services/components/base/ServiceHighlights";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public VPN Plans View
|
* Public VPN Plans View
|
||||||
@ -45,6 +61,45 @@ export function PublicVpnPlansView() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const vpnFeatures: HighlightFeature[] = [
|
||||||
|
{
|
||||||
|
icon: <Router className="h-6 w-6" />,
|
||||||
|
title: "Pre-configured Router",
|
||||||
|
description: "Ready to use out of the box — just plug in and connect",
|
||||||
|
highlight: "Plug & play",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Globe className="h-6 w-6" />,
|
||||||
|
title: "US & UK Servers",
|
||||||
|
description: "Access content from San Francisco or London regions",
|
||||||
|
highlight: "2 locations",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Tv className="h-6 w-6" />,
|
||||||
|
title: "Streaming Ready",
|
||||||
|
description: "Works with Apple TV, Roku, Amazon Fire, and more",
|
||||||
|
highlight: "All devices",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Wifi className="h-6 w-6" />,
|
||||||
|
title: "Separate Network",
|
||||||
|
description: "VPN runs on dedicated WiFi, keep regular internet normal",
|
||||||
|
highlight: "No interference",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Package className="h-6 w-6" />,
|
||||||
|
title: "Router Rental Included",
|
||||||
|
description: "No equipment purchase — router rental is part of the plan",
|
||||||
|
highlight: "No hidden costs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Headphones className="h-6 w-6" />,
|
||||||
|
title: "English Support",
|
||||||
|
description: "Full English assistance for setup and troubleshooting",
|
||||||
|
highlight: "Dedicated help",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto px-4 pb-16">
|
<div className="max-w-6xl mx-auto px-4 pb-16">
|
||||||
<ServicesBackLink href={servicesBasePath} label="Back to Services" />
|
<ServicesBackLink href={servicesBasePath} label="Back to Services" />
|
||||||
@ -52,28 +107,22 @@ export function PublicVpnPlansView() {
|
|||||||
<ServicesHero
|
<ServicesHero
|
||||||
title="VPN Router Service"
|
title="VPN Router Service"
|
||||||
description="Secure VPN connections to San Francisco or London using a pre-configured router."
|
description="Secure VPN connections to San Francisco or London using a pre-configured router."
|
||||||
>
|
/>
|
||||||
{/* Order info banner */}
|
|
||||||
<div className="bg-success-soft border border-success/25 rounded-xl px-4 py-3 max-w-xl mt-4">
|
{/* Service Highlights */}
|
||||||
<div className="flex items-center gap-2 justify-center">
|
<ServiceHighlights features={vpnFeatures} className="mb-12" />
|
||||||
<Zap className="h-4 w-4 text-success flex-shrink-0" />
|
|
||||||
<p className="text-sm text-foreground">
|
|
||||||
<span className="font-medium">Order today</span>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{" "}
|
|
||||||
— create account, add payment, and your router ships upon confirmation.
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ServicesHero>
|
|
||||||
|
|
||||||
{vpnPlans.length > 0 ? (
|
{vpnPlans.length > 0 ? (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h2 className="text-xl font-bold text-foreground mb-2 text-center">Choose Your Region</h2>
|
<div className="text-center mb-8">
|
||||||
<p className="text-sm text-muted-foreground text-center mb-6">
|
<span className="text-sm font-semibold text-primary uppercase tracking-wider">
|
||||||
|
Choose Your Region
|
||||||
|
</span>
|
||||||
|
<h2 className="text-2xl font-bold text-foreground mt-1">Available Plans</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
Select one region per router rental
|
Select one region per router rental
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
||||||
{vpnPlans.map(plan => (
|
{vpnPlans.map(plan => (
|
||||||
@ -103,23 +152,8 @@ export function PublicVpnPlansView() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="bg-card rounded-xl border border-border p-8 mb-8">
|
{/* How It Works Section */}
|
||||||
<h2 className="text-xl font-bold text-foreground mb-6">How It Works</h2>
|
<VpnHowItWorksSection />
|
||||||
<div className="space-y-4 text-sm text-muted-foreground">
|
|
||||||
<p>
|
|
||||||
SonixNet VPN is the easiest way to access video streaming services from overseas on your
|
|
||||||
network media players such as an Apple TV, Roku, or Amazon Fire.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
A configured Wi-Fi router is provided for rental (no purchase required, no hidden fees).
|
|
||||||
All you need to do is plug the VPN router into your existing internet connection.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Connect your network media players to the VPN Wi-Fi network to access content from the
|
|
||||||
selected region. For regular internet usage, use your normal home Wi-Fi.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* FAQ Section */}
|
{/* FAQ Section */}
|
||||||
<VpnFaqSection />
|
<VpnFaqSection />
|
||||||
@ -196,4 +230,102 @@ function VpnFaqSection() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HowItWorksStepProps {
|
||||||
|
number: number;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HowItWorksStep({ number, icon, title, description }: HowItWorksStepProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center text-center flex-1 min-w-0">
|
||||||
|
{/* Icon with number badge */}
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<div className="flex h-16 w-16 items-center justify-center rounded-xl bg-gray-50 border border-gray-200 text-primary shadow-sm">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
{/* Number badge */}
|
||||||
|
<div className="absolute -top-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-primary text-white text-xs font-bold shadow-sm">
|
||||||
|
{number}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<h4 className="font-semibold text-foreground mb-2">{title}</h4>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed max-w-[180px]">{description}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VpnHowItWorksSection() {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
icon: <CreditCard className="h-6 w-6" />,
|
||||||
|
title: "Sign Up",
|
||||||
|
description: "Create your account to get started",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Globe className="h-6 w-6" />,
|
||||||
|
title: "Choose Region",
|
||||||
|
description: "Select US (San Francisco) or UK (London)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Package className="h-6 w-6" />,
|
||||||
|
title: "Place Order",
|
||||||
|
description: "Complete checkout and receive router",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Play className="h-6 w-6" />,
|
||||||
|
title: "Connect & Stream",
|
||||||
|
description: "Plug in, connect devices, enjoy",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] p-8 mb-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<span className="text-sm font-semibold text-primary uppercase tracking-wider">
|
||||||
|
Simple Setup
|
||||||
|
</span>
|
||||||
|
<h3 className="text-2xl font-bold text-foreground mt-1">How It Works</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Steps with connecting line */}
|
||||||
|
<div className="relative">
|
||||||
|
{/* Connecting line - hidden on mobile */}
|
||||||
|
<div className="hidden md:block absolute top-8 left-[12%] right-[12%] h-0.5 bg-gray-200" />
|
||||||
|
|
||||||
|
{/* Curved path SVG for visual connection - hidden on mobile */}
|
||||||
|
<svg
|
||||||
|
className="hidden md:block absolute top-[30px] left-0 right-0 w-full h-4 pointer-events-none"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M 12% 8 Q 30% 8, 37.5% 8 Q 45% 8, 50% 8 Q 55% 8, 62.5% 8 Q 70% 8, 88% 8"
|
||||||
|
fill="none"
|
||||||
|
stroke="#e5e7eb"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeDasharray="6 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Steps grid */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8 relative z-10">
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<HowItWorksStep
|
||||||
|
key={index}
|
||||||
|
number={index + 1}
|
||||||
|
icon={step.icon}
|
||||||
|
title={step.title}
|
||||||
|
description={step.description}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default PublicVpnPlansView;
|
export default PublicVpnPlansView;
|
||||||
|
|||||||
87
docs/testing/sim-api-test-tracking.md
Normal file
87
docs/testing/sim-api-test-tracking.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# SIM API Test Tracking
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The test tracking system automatically logs all Freebit API calls made during physical SIM testing to a CSV file. This allows you to track which APIs were tested on which phones and at what time.
|
||||||
|
|
||||||
|
## Log File Location
|
||||||
|
|
||||||
|
The test tracking log file is created at the project root:
|
||||||
|
|
||||||
|
- **File**: `sim-api-test-log.csv`
|
||||||
|
- **Location**: Project root directory
|
||||||
|
|
||||||
|
## Log Format
|
||||||
|
|
||||||
|
The CSV file contains the following columns:
|
||||||
|
|
||||||
|
1. **Timestamp** - ISO 8601 timestamp of when the API was called
|
||||||
|
2. **API Endpoint** - The Freebit API endpoint (e.g., `/mvno/getDetail/`, `/master/addSpec/`)
|
||||||
|
3. **API Method** - HTTP method (typically "POST")
|
||||||
|
4. **Phone Number** - The phone number/SIM account identifier
|
||||||
|
5. **SIM Identifier** - Additional SIM identifier if available
|
||||||
|
6. **Request Payload** - JSON string of the request (sensitive data redacted)
|
||||||
|
7. **Response Status** - Success or error status code
|
||||||
|
8. **Error** - Error message if the call failed
|
||||||
|
9. **Additional Info** - Additional status information
|
||||||
|
|
||||||
|
## What Gets Tracked
|
||||||
|
|
||||||
|
The system automatically tracks all Freebit API calls that include a phone number/account identifier, including:
|
||||||
|
|
||||||
|
- **Physical SIM Activation** (`/master/addSpec/` or `/mvno/eachQuota/`)
|
||||||
|
- **SIM Top-up** (`/master/addSpec/` or `/mvno/eachQuota/`)
|
||||||
|
- **SIM Details** (`/mvno/getDetail/`)
|
||||||
|
- **SIM Usage** (`/mvno/getTrafficInfo/`)
|
||||||
|
- **Plan Changes** (`/mvno/changePlan/`)
|
||||||
|
- **SIM Cancellation** (`/mvno/releasePlan/`)
|
||||||
|
- **eSIM Operations** (`/esim/reissueProfile/`, `/mvno/esim/addAcnt/`)
|
||||||
|
- **Quota History** (`/mvno/getQuotaHistory/`)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The tracking is **automatic** - no code changes needed. Every time a Freebit API is called with a phone number, it will be logged to the CSV file.
|
||||||
|
|
||||||
|
### Example Log Entry
|
||||||
|
|
||||||
|
```csv
|
||||||
|
Timestamp,API Endpoint,API Method,Phone Number,SIM Identifier,Request Payload,Response Status,Error,Additional Info
|
||||||
|
2025-01-15T10:30:45.123Z,/master/addSpec/,POST,02000002470001,02000002470001,"{""account"":""02000002470001"",""quota"":0}",Success,,"OK"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Data Files
|
||||||
|
|
||||||
|
The following CSV files contain test SIM data:
|
||||||
|
|
||||||
|
- `ASI_N6_PASI_20251229.csv` - 半黒データ (Half-black data) - 50 SIMs (rows 1-50)
|
||||||
|
- `ASI_N7_PASI_20251229.csv` - 半黒音声 (Half-black voice) - 50 SIMs (rows 51-100)
|
||||||
|
|
||||||
|
Each row contains:
|
||||||
|
|
||||||
|
- Row number
|
||||||
|
- SIM number (phone number)
|
||||||
|
- PT number
|
||||||
|
- PASI identifier
|
||||||
|
- Date (20251229)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The tracking system only logs API calls that include a phone number/account identifier
|
||||||
|
- Tracking failures do not affect API functionality - if logging fails, the API call still proceeds
|
||||||
|
- The log file is append-only - new entries are added to the end of the file
|
||||||
|
- Request payloads are redacted for security (sensitive data like auth keys are removed)
|
||||||
|
|
||||||
|
## Viewing the Log
|
||||||
|
|
||||||
|
You can view the log file using any CSV viewer or text editor:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View the log file
|
||||||
|
cat sim-api-test-log.csv
|
||||||
|
|
||||||
|
# View last 20 entries
|
||||||
|
tail -20 sim-api-test-log.csv
|
||||||
|
|
||||||
|
# Filter by phone number
|
||||||
|
grep "02000002470001" sim-api-test-log.csv
|
||||||
|
```
|
||||||
Loading…
x
Reference in New Issue
Block a user