2026-01-13 14:25:14 +09:00
|
|
|
import { Injectable, Inject, Optional } from "@nestjs/common";
|
2025-11-21 18:41:14 +09:00
|
|
|
import { Logger } from "nestjs-pino";
|
2026-02-24 19:05:30 +09:00
|
|
|
import { FreebitOperationException } from "@bff/core/exceptions/domain-exceptions.js";
|
2025-11-21 18:41:14 +09:00
|
|
|
import type {
|
|
|
|
|
FreebitAccountDetailsResponse,
|
|
|
|
|
FreebitTrafficInfoResponse,
|
|
|
|
|
FreebitQuotaHistoryResponse,
|
|
|
|
|
SimDetails,
|
|
|
|
|
SimUsage,
|
|
|
|
|
SimTopUpHistory,
|
2025-12-10 16:08:34 +09:00
|
|
|
} from "../interfaces/freebit.types.js";
|
2026-01-13 14:25:14 +09:00
|
|
|
import type { VoiceOptionsService } from "@bff/modules/voice-options/services/voice-options.service.js";
|
2025-11-21 18:41:14 +09:00
|
|
|
|
refactor: fix all lint errors and reduce warnings across BFF and domain
Eliminate all 12 ESLint errors (nested ternaries, any types) and reduce
warnings by 13 (duplicate strings, complexity). Key changes:
- Domain: extract helpers for nested ternaries in opportunity/contract and whmcs/mapper
- BFF core: fix any type in safe-operation.util, refactor exception filter to use
options objects, create shared CACHE_CONTROL and normalizeToArray utilities
- Freebit: replace nested ternaries with if/else in client and mapper services
- Sim fulfillment: extract helper methods to reduce complexity (fulfillEsim,
fulfillPhysicalSim, buildMnpPayload, registerVoiceOptionsIfAvailable, MNP_FIELD_MAPPINGS)
- Modules: fix 8 nested ternary violations across validators, services, controllers
- Constants: extract duplicate strings (CSRF, email, orchestrator, cache control)
2026-03-04 10:52:26 +09:00
|
|
|
const NOT_IN_API_RESPONSE = "(not in API response)";
|
|
|
|
|
|
2025-11-21 18:41:14 +09:00
|
|
|
@Injectable()
|
|
|
|
|
export class FreebitMapperService {
|
|
|
|
|
constructor(
|
|
|
|
|
@Inject(Logger) private readonly logger: Logger,
|
2026-01-13 14:25:14 +09:00
|
|
|
@Optional()
|
|
|
|
|
@Inject("SimVoiceOptionsService")
|
|
|
|
|
private readonly voiceOptionsService?: VoiceOptionsService
|
2025-11-21 18:41:14 +09:00
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
private parseOptionFlag(value: unknown, defaultValue: boolean = false): boolean {
|
|
|
|
|
// If value is undefined or null, return the default
|
|
|
|
|
if (value === undefined || value === null) {
|
|
|
|
|
return defaultValue;
|
|
|
|
|
}
|
2025-12-10 16:08:34 +09:00
|
|
|
|
2025-11-21 18:41:14 +09:00
|
|
|
if (typeof value === "boolean") {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
if (typeof value === "number") {
|
|
|
|
|
return value === 10 || value === 1;
|
|
|
|
|
}
|
|
|
|
|
if (typeof value === "string") {
|
|
|
|
|
const normalized = value.trim().toLowerCase();
|
|
|
|
|
if (normalized === "on" || normalized === "true") {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (normalized === "off" || normalized === "false") {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const numeric = Number(normalized);
|
|
|
|
|
if (!Number.isNaN(numeric)) {
|
|
|
|
|
return numeric === 10 || numeric === 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return defaultValue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map SIM status from Freebit API to domain status
|
|
|
|
|
*/
|
|
|
|
|
mapSimStatus(status: string): "active" | "suspended" | "cancelled" | "pending" {
|
|
|
|
|
switch (status) {
|
|
|
|
|
case "active":
|
|
|
|
|
return "active";
|
|
|
|
|
case "suspended":
|
|
|
|
|
return "suspended";
|
|
|
|
|
case "temporary":
|
|
|
|
|
case "waiting":
|
|
|
|
|
return "pending";
|
|
|
|
|
case "obsolete":
|
|
|
|
|
return "cancelled";
|
|
|
|
|
default:
|
|
|
|
|
return "pending";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Freebit account details response to SimDetails
|
|
|
|
|
*/
|
|
|
|
|
async mapToSimDetails(response: FreebitAccountDetailsResponse): Promise<SimDetails> {
|
|
|
|
|
const account = response.responseDatas[0];
|
|
|
|
|
if (!account) {
|
2026-02-24 19:05:30 +09:00
|
|
|
throw new FreebitOperationException("No account data in response");
|
2025-11-21 18:41:14 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 17:05:54 +09:00
|
|
|
// Debug: Log raw voice option fields from API response
|
|
|
|
|
this.logger.debug("[FreebitMapper] Raw API voice option fields", {
|
|
|
|
|
account: account.account,
|
|
|
|
|
voicemail: account.voicemail,
|
|
|
|
|
voiceMail: account.voiceMail,
|
|
|
|
|
callwaiting: account.callwaiting,
|
|
|
|
|
callWaiting: account.callWaiting,
|
|
|
|
|
worldwing: account.worldwing,
|
|
|
|
|
worldWing: account.worldWing,
|
|
|
|
|
talk: account.talk,
|
|
|
|
|
sms: account.sms,
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-21 18:41:14 +09:00
|
|
|
let simType: "standard" | "nano" | "micro" | "esim" = "standard";
|
|
|
|
|
if (account.eid) {
|
|
|
|
|
simType = "esim";
|
|
|
|
|
} else if (account.simSize) {
|
|
|
|
|
simType = account.simSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to get voice options from database first
|
2026-02-02 17:05:54 +09:00
|
|
|
// Default to false - show as disabled unless API confirms enabled
|
|
|
|
|
let voiceMailEnabled = false;
|
|
|
|
|
let callWaitingEnabled = false;
|
|
|
|
|
let internationalRoamingEnabled = false;
|
2025-11-21 18:41:14 +09:00
|
|
|
let networkType = String(account.networkType ?? account.contractLine ?? "4G");
|
|
|
|
|
|
2026-02-02 17:05:54 +09:00
|
|
|
// Try to load stored options from database first
|
|
|
|
|
let storedOptions = null;
|
2025-11-21 18:41:14 +09:00
|
|
|
if (this.voiceOptionsService) {
|
|
|
|
|
try {
|
2026-02-02 17:05:54 +09:00
|
|
|
storedOptions = await this.voiceOptionsService.getVoiceOptions(
|
2025-11-21 18:41:14 +09:00
|
|
|
String(account.account ?? "")
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.warn("[FreebitMapper] Failed to load voice options from database", {
|
|
|
|
|
account: account.account,
|
|
|
|
|
error,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 17:05:54 +09:00
|
|
|
if (storedOptions) {
|
|
|
|
|
// Use stored options from database
|
|
|
|
|
voiceMailEnabled = storedOptions.voiceMailEnabled;
|
|
|
|
|
callWaitingEnabled = storedOptions.callWaitingEnabled;
|
|
|
|
|
internationalRoamingEnabled = storedOptions.internationalRoamingEnabled;
|
|
|
|
|
networkType = storedOptions.networkType;
|
|
|
|
|
|
|
|
|
|
this.logger.debug("[FreebitMapper] Loaded voice options from database", {
|
|
|
|
|
account: account.account,
|
|
|
|
|
options: storedOptions,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// No stored options, parse from API response
|
|
|
|
|
// Default to false - disabled unless API explicitly returns 10 (enabled)
|
|
|
|
|
voiceMailEnabled = this.parseOptionFlag(account.voicemail ?? account.voiceMail, false);
|
2026-02-03 17:35:47 +09:00
|
|
|
callWaitingEnabled = this.parseOptionFlag(account.callwaiting ?? account.callWaiting, false);
|
2026-02-02 17:05:54 +09:00
|
|
|
internationalRoamingEnabled = this.parseOptionFlag(
|
|
|
|
|
account.worldwing ?? account.worldWing,
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.logger.debug(
|
|
|
|
|
"[FreebitMapper] No stored options found, using API values (default: disabled)",
|
|
|
|
|
{
|
|
|
|
|
account: account.account,
|
refactor: fix all lint errors and reduce warnings across BFF and domain
Eliminate all 12 ESLint errors (nested ternaries, any types) and reduce
warnings by 13 (duplicate strings, complexity). Key changes:
- Domain: extract helpers for nested ternaries in opportunity/contract and whmcs/mapper
- BFF core: fix any type in safe-operation.util, refactor exception filter to use
options objects, create shared CACHE_CONTROL and normalizeToArray utilities
- Freebit: replace nested ternaries with if/else in client and mapper services
- Sim fulfillment: extract helper methods to reduce complexity (fulfillEsim,
fulfillPhysicalSim, buildMnpPayload, registerVoiceOptionsIfAvailable, MNP_FIELD_MAPPINGS)
- Modules: fix 8 nested ternary violations across validators, services, controllers
- Constants: extract duplicate strings (CSRF, email, orchestrator, cache control)
2026-03-04 10:52:26 +09:00
|
|
|
rawVoiceMail: account.voicemail ?? account.voiceMail ?? NOT_IN_API_RESPONSE,
|
|
|
|
|
rawCallWaiting: account.callwaiting ?? account.callWaiting ?? NOT_IN_API_RESPONSE,
|
|
|
|
|
rawWorldWing: account.worldwing ?? account.worldWing ?? NOT_IN_API_RESPONSE,
|
2026-02-02 17:05:54 +09:00
|
|
|
parsedVoiceMailEnabled: voiceMailEnabled,
|
|
|
|
|
parsedCallWaitingEnabled: callWaitingEnabled,
|
|
|
|
|
parsedInternationalRoamingEnabled: internationalRoamingEnabled,
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 15:57:50 +09:00
|
|
|
// Convert quota from KB to MB if needed
|
|
|
|
|
// Freebit API returns quota in KB, but remainingQuotaMb should be in MB
|
|
|
|
|
let remainingQuotaMb = 0;
|
|
|
|
|
let remainingQuotaKb = 0;
|
|
|
|
|
|
|
|
|
|
if (account.remainingQuotaMb != null) {
|
|
|
|
|
// If API explicitly provides remainingQuotaMb, use it directly
|
|
|
|
|
remainingQuotaMb = Number(account.remainingQuotaMb);
|
fix: comprehensive SIM audit fixes and MNP debug logging
Address critical, high, and medium issues found during SIM management audit:
Critical: fix eSIM plan code mapping (SKU→PASI), PA05-41 endpoint typo,
PA05-05 gender mapping (F→W) and katakana field names.
High: fix double authKey injection, add MNP/porting fields to SF getOrder
SOQL, add reissue params to eSIM addAcnt, remove console.error debug stmt.
Medium: fix KB/MB conversion (1000→1024), birthday UTC timezone bug, plan
code regex matching "5G" as 5GB, case-insensitive isMnp flag, domain schema
enums (addKind +M, simkind E0/E2/E3), move identity into mnp Level 2.
Frontend: fix SVG donut radius mismatch (r=88→96), fix FreebitError typo.
Add comprehensive MNP debug logging across the entire data flow pipeline:
SF order extraction, config mapping, MNP field parsing, API payload assembly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:48:50 +09:00
|
|
|
remainingQuotaKb = Number(account.remainingQuotaKb ?? remainingQuotaMb * 1024);
|
2026-01-05 15:57:50 +09:00
|
|
|
} else if (account.quota != null) {
|
fix: comprehensive SIM audit fixes and MNP debug logging
Address critical, high, and medium issues found during SIM management audit:
Critical: fix eSIM plan code mapping (SKU→PASI), PA05-41 endpoint typo,
PA05-05 gender mapping (F→W) and katakana field names.
High: fix double authKey injection, add MNP/porting fields to SF getOrder
SOQL, add reissue params to eSIM addAcnt, remove console.error debug stmt.
Medium: fix KB/MB conversion (1000→1024), birthday UTC timezone bug, plan
code regex matching "5G" as 5GB, case-insensitive isMnp flag, domain schema
enums (addKind +M, simkind E0/E2/E3), move identity into mnp Level 2.
Frontend: fix SVG donut radius mismatch (r=88→96), fix FreebitError typo.
Add comprehensive MNP debug logging across the entire data flow pipeline:
SF order extraction, config mapping, MNP field parsing, API payload assembly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:48:50 +09:00
|
|
|
// If only quota is provided, it's in KB - convert to MB (1 MB = 1024 KB)
|
2026-01-05 15:57:50 +09:00
|
|
|
remainingQuotaKb = Number(account.quota);
|
fix: comprehensive SIM audit fixes and MNP debug logging
Address critical, high, and medium issues found during SIM management audit:
Critical: fix eSIM plan code mapping (SKU→PASI), PA05-41 endpoint typo,
PA05-05 gender mapping (F→W) and katakana field names.
High: fix double authKey injection, add MNP/porting fields to SF getOrder
SOQL, add reissue params to eSIM addAcnt, remove console.error debug stmt.
Medium: fix KB/MB conversion (1000→1024), birthday UTC timezone bug, plan
code regex matching "5G" as 5GB, case-insensitive isMnp flag, domain schema
enums (addKind +M, simkind E0/E2/E3), move identity into mnp Level 2.
Frontend: fix SVG donut radius mismatch (r=88→96), fix FreebitError typo.
Add comprehensive MNP debug logging across the entire data flow pipeline:
SF order extraction, config mapping, MNP field parsing, API payload assembly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:48:50 +09:00
|
|
|
remainingQuotaMb = remainingQuotaKb / 1024;
|
2026-01-05 15:57:50 +09:00
|
|
|
} else if (account.remainingQuotaKb != null) {
|
fix: comprehensive SIM audit fixes and MNP debug logging
Address critical, high, and medium issues found during SIM management audit:
Critical: fix eSIM plan code mapping (SKU→PASI), PA05-41 endpoint typo,
PA05-05 gender mapping (F→W) and katakana field names.
High: fix double authKey injection, add MNP/porting fields to SF getOrder
SOQL, add reissue params to eSIM addAcnt, remove console.error debug stmt.
Medium: fix KB/MB conversion (1000→1024), birthday UTC timezone bug, plan
code regex matching "5G" as 5GB, case-insensitive isMnp flag, domain schema
enums (addKind +M, simkind E0/E2/E3), move identity into mnp Level 2.
Frontend: fix SVG donut radius mismatch (r=88→96), fix FreebitError typo.
Add comprehensive MNP debug logging across the entire data flow pipeline:
SF order extraction, config mapping, MNP field parsing, API payload assembly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:48:50 +09:00
|
|
|
// If only remainingQuotaKb is provided, convert to MB (1 MB = 1024 KB)
|
2026-01-05 15:57:50 +09:00
|
|
|
remainingQuotaKb = Number(account.remainingQuotaKb);
|
fix: comprehensive SIM audit fixes and MNP debug logging
Address critical, high, and medium issues found during SIM management audit:
Critical: fix eSIM plan code mapping (SKU→PASI), PA05-41 endpoint typo,
PA05-05 gender mapping (F→W) and katakana field names.
High: fix double authKey injection, add MNP/porting fields to SF getOrder
SOQL, add reissue params to eSIM addAcnt, remove console.error debug stmt.
Medium: fix KB/MB conversion (1000→1024), birthday UTC timezone bug, plan
code regex matching "5G" as 5GB, case-insensitive isMnp flag, domain schema
enums (addKind +M, simkind E0/E2/E3), move identity into mnp Level 2.
Frontend: fix SVG donut radius mismatch (r=88→96), fix FreebitError typo.
Add comprehensive MNP debug logging across the entire data flow pipeline:
SF order extraction, config mapping, MNP field parsing, API payload assembly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:48:50 +09:00
|
|
|
remainingQuotaMb = remainingQuotaKb / 1024;
|
2026-01-05 15:57:50 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 17:05:54 +09:00
|
|
|
// Log raw account data in dev to debug MSISDN availability
|
2026-02-03 17:35:47 +09:00
|
|
|
if (process.env["NODE_ENV"] !== "production") {
|
|
|
|
|
this.logger.debug("[FREEBIT ACCOUNT DATA]", {
|
2026-02-02 17:05:54 +09:00
|
|
|
account: account.account,
|
|
|
|
|
msisdn: account.msisdn,
|
|
|
|
|
eid: account.eid,
|
|
|
|
|
iccid: account.iccid,
|
2026-02-03 17:35:47 +09:00
|
|
|
});
|
2026-02-02 17:05:54 +09:00
|
|
|
}
|
|
|
|
|
|
2025-11-21 18:41:14 +09:00
|
|
|
return {
|
|
|
|
|
account: String(account.account ?? ""),
|
|
|
|
|
status: this.mapSimStatus(String(account.state ?? account.status ?? "pending")),
|
|
|
|
|
planCode: String(account.planCode ?? ""),
|
|
|
|
|
planName: String(account.planName ?? ""),
|
|
|
|
|
simType,
|
|
|
|
|
iccid: String(account.iccid ?? ""),
|
|
|
|
|
eid: String(account.eid ?? ""),
|
|
|
|
|
msisdn: String(account.msisdn ?? account.account ?? ""),
|
|
|
|
|
imsi: String(account.imsi ?? ""),
|
2026-01-05 15:57:50 +09:00
|
|
|
remainingQuotaMb,
|
|
|
|
|
remainingQuotaKb,
|
2025-11-21 18:41:14 +09:00
|
|
|
voiceMailEnabled,
|
|
|
|
|
callWaitingEnabled,
|
|
|
|
|
internationalRoamingEnabled,
|
|
|
|
|
networkType,
|
|
|
|
|
activatedAt: account.startDate ? String(account.startDate) : undefined,
|
|
|
|
|
expiresAt: account.async ? String(account.async.date) : undefined,
|
|
|
|
|
ipv4: account.ipv4,
|
|
|
|
|
ipv6: account.ipv6,
|
|
|
|
|
startDate: account.startDate ? String(account.startDate) : undefined,
|
|
|
|
|
hasVoice: account.talk === 10,
|
|
|
|
|
hasSms: account.sms === 10,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Freebit traffic info response to SimUsage
|
|
|
|
|
*/
|
|
|
|
|
mapToSimUsage(response: FreebitTrafficInfoResponse): SimUsage {
|
|
|
|
|
if (!response.traffic) {
|
2026-02-24 19:05:30 +09:00
|
|
|
throw new FreebitOperationException("No traffic data in response");
|
2025-11-21 18:41:14 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-15 11:28:25 +09:00
|
|
|
const todayUsageKb = Number.parseInt(response.traffic.today, 10) || 0;
|
2025-11-21 18:41:14 +09:00
|
|
|
const recentDaysData = response.traffic.inRecentDays.split(",").map((usage, index) => ({
|
2026-01-15 11:28:25 +09:00
|
|
|
date:
|
|
|
|
|
new Date(Date.now() - (index + 1) * 24 * 60 * 60 * 1000).toISOString().split("T")[0] ?? "",
|
|
|
|
|
usageKb: Number.parseInt(usage, 10) || 0,
|
|
|
|
|
usageMb: Math.round(((Number.parseInt(usage, 10) || 0) / 1024) * 100) / 100,
|
2025-11-21 18:41:14 +09:00
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
account: String(response.account ?? ""),
|
|
|
|
|
todayUsageMb: Math.round((todayUsageKb / 1024) * 100) / 100,
|
|
|
|
|
todayUsageKb,
|
|
|
|
|
recentDaysUsage: recentDaysData,
|
|
|
|
|
isBlacklisted: response.traffic.blackList === "10",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Map Freebit quota history response to SimTopUpHistory
|
|
|
|
|
*/
|
|
|
|
|
mapToSimTopUpHistory(response: FreebitQuotaHistoryResponse, account: string): SimTopUpHistory {
|
|
|
|
|
if (!response.quotaHistory) {
|
2026-02-24 19:05:30 +09:00
|
|
|
throw new FreebitOperationException("No history data in response");
|
2025-11-21 18:41:14 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
account,
|
|
|
|
|
totalAdditions: Number(response.total) || 0,
|
|
|
|
|
additionCount: Number(response.count) || 0,
|
|
|
|
|
history: response.quotaHistory.map(item => ({
|
2026-01-15 11:28:25 +09:00
|
|
|
quotaKb: Number.parseInt(item.quota, 10),
|
|
|
|
|
quotaMb: Math.round((Number.parseInt(item.quota, 10) / 1024) * 100) / 100,
|
2025-11-21 18:41:14 +09:00
|
|
|
addedDate: item.date,
|
|
|
|
|
expiryDate: item.expire,
|
|
|
|
|
campaignCode: item.quotaCode,
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Normalize account identifier (remove formatting)
|
|
|
|
|
*/
|
|
|
|
|
normalizeAccount(account: string): string {
|
|
|
|
|
return account.replace(/[-\s()]/g, "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate account format
|
|
|
|
|
*/
|
|
|
|
|
validateAccount(account: string): boolean {
|
|
|
|
|
const normalized = this.normalizeAccount(account);
|
|
|
|
|
// Basic validation - should be digits, typically 10-11 digits for Japanese phone numbers
|
|
|
|
|
return /^\d{10,11}$/.test(normalized);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Format date for Freebit API (YYYYMMDD)
|
|
|
|
|
*/
|
|
|
|
|
formatDateForApi(date: Date): string {
|
|
|
|
|
const year = date.getFullYear();
|
|
|
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
|
|
|
return `${year}${month}${day}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Parse date from Freebit API format (YYYYMMDD)
|
|
|
|
|
*/
|
|
|
|
|
parseDateFromApi(dateString: string): Date | null {
|
|
|
|
|
if (!/^\d{8}$/.test(dateString)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 11:28:25 +09:00
|
|
|
const year = Number.parseInt(dateString.slice(0, 4), 10);
|
|
|
|
|
const month = Number.parseInt(dateString.slice(4, 6), 10) - 1; // Month is 0-indexed
|
|
|
|
|
const day = Number.parseInt(dateString.slice(6, 8), 10);
|
2025-11-21 18:41:14 +09:00
|
|
|
|
|
|
|
|
return new Date(year, month, day);
|
|
|
|
|
}
|
|
|
|
|
}
|