diff --git a/apps/bff/sim-api-test-log.csv b/apps/bff/sim-api-test-log.csv index 2fa55d66..f59e6de4 100644 --- a/apps/bff/sim-api-test-log.csv +++ b/apps/bff/sim-api-test-log.csv @@ -1 +1,3 @@ Timestamp,API Endpoint,API Method,Phone Number,SIM Identifier,Request Payload,Response Status,Error,Additional Info +2026-01-19T04:05:41.856Z,/mvno/getTrafficInfo/,POST,02000524104652,02000524104652,"{""account"":""02000524104652""}",Success,,OK +2026-01-19T04:05:41.945Z,/mvno/getTrafficInfo/,POST,02000524104652,02000524104652,"{""account"":""02000524104652""}",Success,,OK diff --git a/apps/portal/src/app/(public)/(site)/services/onsite/OnsiteSupportContent.tsx b/apps/portal/src/app/(public)/(site)/services/onsite/OnsiteSupportContent.tsx new file mode 100644 index 00000000..8baa6c64 --- /dev/null +++ b/apps/portal/src/app/(public)/(site)/services/onsite/OnsiteSupportContent.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/atoms"; +import { Users, Monitor, Tv, Headset, ChevronDown } from "lucide-react"; + +export function OnsiteSupportContent() { + return ( +
+ {/* Header */} +
+

+ Onsite Support +

+

+ We dispatch our skillful in-house tech staff to your residence or office for your needs. +

+
+ + {/* Main Services */} +
+
+

Need Our Technical Support?

+

+ We can provide you with on-site technical support service. If you would like for our + technicians to visit your residence and provide technical assistance, please let us + know. +

+

+ We also provide "Remote Access Services" which allows our technicians to do + support via Remote Access Software over the Internet connection to fix up the issue + (depends on what the issue is). +

+
+ +
+
+
+ +
+
+ + {/* Pricing Cards */} +
+ {/* Onsite Network & Computer Support */} +
+
+ +
+

+ Onsite Network & Computer Support +

+
+
Basic Service Fee
+
15,000 JPY
+
+
+ + {/* Remote Support */} +
+
+ +
+

+ Remote Network & Computer Support +

+
+
Basic Service Fee
+
5,000 JPY
+
+
+ + {/* Onsite TV Support */} +
+
+ +
+

Onsite TV Support Service

+
+
Basic Service Fee
+
15,000 JPY
+
+
+
+ + {/* FAQ Section */} +
+

+ Frequently Asked Questions +

+
+ + Yes, the Assist Solutions technical team is able to visit your residence for device + set up including Wi-Fi routers, printers, Apple TVs etc. +
+
+ Our tech consulting team will be able to make suggestions based on your residence + layout and requirements. Please contact us at info@asolutions.co.jp for a free + consultation. + + } + /> + + + Our In-Home Technical Assistance service can be provided in Tokyo, Saitama and + Kanagawa prefecture. +
+
+ *Please note that this service may not available in some areas within the above + prefectures. +
+ For more information, please contact us at info@asolutions.co.jp + + } + /> +
+
+ + {/* CTA */} +
+

Ready to get started?

+ +
+
+ ); +} + +/** + * FAQ Item component with expand/collapse functionality + */ +function FaqItem({ question, answer }: { question: string; answer: React.ReactNode }) { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ + {isOpen && ( +
{answer}
+ )} +
+ ); +} diff --git a/apps/portal/src/app/(public)/(site)/services/onsite/page.tsx b/apps/portal/src/app/(public)/(site)/services/onsite/page.tsx index c85c39f0..be495036 100644 --- a/apps/portal/src/app/(public)/(site)/services/onsite/page.tsx +++ b/apps/portal/src/app/(public)/(site)/services/onsite/page.tsx @@ -1,6 +1,5 @@ import type { Metadata } from "next"; -import { Button } from "@/components/atoms"; -import { Users, Monitor, Tv, Headset } from "lucide-react"; +import { OnsiteSupportContent } from "./OnsiteSupportContent"; export const metadata: Metadata = { title: "Onsite Support - Tech Support in Tokyo | Assist Solutions", @@ -21,93 +20,5 @@ export const metadata: Metadata = { }; export default function OnsiteSupportPage() { - return ( -
- {/* Header */} -
-

- Onsite Support -

-

- We dispatch our skillful in-house tech staff to your residence or office for your needs. -

-
- - {/* Main Services */} -
-
-

Need Our Technical Support?

-

- We can provide you with on-site technical support service. If you would like for our - technicians to visit your residence and provide technical assistance, please let us - know. -

-

- We also provide "Remote Access Services" which allows our technicians to do support via - Remote Access Software over the Internet connection to fix up the issue (depends on what - the issue is). -

-
- -
-
-
- -
-
- - {/* Pricing Cards */} -
- {/* Onsite Network & Computer Support */} -
-
- -
-

- Onsite Network & Computer Support -

-
-
Basic Service Fee
-
15,000 JPY
-
-
- - {/* Remote Support */} -
-
- -
-

- Remote Network & Computer Support -

-
-
Basic Service Fee
-
5,000 JPY
-
-
- - {/* Onsite TV Support */} -
-
- -
-

Onsite TV Support Service

-
-
Basic Service Fee
-
15,000 JPY
-
-
-
- - {/* CTA */} -
-

Ready to get started?

- -
-
- ); + return ; } diff --git a/apps/portal/src/app/(public)/(site)/services/tv/page.tsx b/apps/portal/src/app/(public)/(site)/services/tv/page.tsx index c57527a4..51915c89 100644 --- a/apps/portal/src/app/(public)/(site)/services/tv/page.tsx +++ b/apps/portal/src/app/(public)/(site)/services/tv/page.tsx @@ -670,6 +670,23 @@ export default function TVServicesPage() { + {/* FAQ Section */} +
+

+ Frequently Asked Questions +

+
+ + +
+
+ {/* CTA */}

@@ -683,3 +700,28 @@ export default function TVServicesPage() {

); } + +/** + * FAQ Item component with expand/collapse functionality + */ +function FaqItem({ question, answer }: { question: string; answer: string }) { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ + {isOpen && ( +
{answer}
+ )} +
+ ); +} diff --git a/apps/portal/src/features/services/components/sim/DeviceCompatibility.tsx b/apps/portal/src/features/services/components/sim/DeviceCompatibility.tsx new file mode 100644 index 00000000..85935d4d --- /dev/null +++ b/apps/portal/src/features/services/components/sim/DeviceCompatibility.tsx @@ -0,0 +1,344 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { Search, Smartphone, Check, X } from "lucide-react"; +import { cn } from "@/shared/utils"; + +// Device categories with their devices +const DEVICE_CATEGORIES = [ + { + name: "Apple iPhone", + devices: [ + "iPhone 16 Series (Standard/Plus/Pro/Pro Max)", + "iPhone 15 Series (Standard/Plus/Pro/Pro Max)", + "iPhone 14 Series (Standard/Plus/Pro/Pro Max)", + "iPhone SE (3rd Generation, 2022)", + "iPhone 13 Series (Standard/Mini/Pro/Pro Max)", + "iPhone 12 Series (Standard/Mini/Pro/Pro Max)", + "iPhone SE (2nd Generation, 2020)", + "iPhone 11 Series (Standard/Pro/Pro Max)", + "iPhone XS Series (Standard/Max)", + "iPhone XR", + "iPhone X (4G Only)", + "iPhone 8 / 8 Plus (4G Only)", + "iPhone 7 / 7 Plus (4G Only)", + "iPhone 6s / 6s Plus (4G Only)", + ], + }, + { + name: "Apple iPad", + devices: [ + "iPad Pro 13-inch (M4)", + 'iPad Pro 12.9" (6th/5th/4th/3rd Generations)', + 'iPad Pro 11" (4th/3rd/2nd/1st Generations)', + "iPad Air 13-inch (M2)", + "iPad Air (5th/4th/3rd Generations)", + "iPad Mini (6th/5th Generations, A17 Pro)", + "iPad Standard (10th/9th/8th/7th Generations)", + ], + }, + { + name: "Google Pixel", + devices: [ + "Pixel 9 Series (Pro XL/Pro/Fold/Standard)", + "Pixel 8 / 8a / 8 Pro", + "Pixel 7a / 7 / 7 Pro", + "Pixel Fold", + "Pixel 6a / 6 / 6 Pro", + "Pixel 5a (5G) / 5", + "Pixel 4a (5G) / 4a", + "Pixel 4 XL / 4", + "Pixel 3a XL / 3a", + "Pixel 3 XL / 3", + ], + }, + { + name: "Samsung Galaxy S Series", + devices: [ + "Galaxy S25 Edge", + "Galaxy S24 Ultra / S24 / S24 FE", + "Galaxy S23 Ultra / S23 / S23 FE", + "Galaxy S22 Ultra 5G / S22+ 5G / S22 5G", + "Galaxy S21 Ultra 5G / S21+ 5G / S21 5G", + "Galaxy S20 Ultra / S20+ 5G / S20+ / S20 5G / S20", + "Galaxy S10", + "Galaxy S7 edge / S6 / S6 edge", + "Galaxy S5 ACTIVE / S5 / S4", + ], + }, + { + name: "Samsung Galaxy Z / Note / A Series", + devices: [ + "Galaxy Z Fold 6 / 5 / 4 / 3 / 2", + "Galaxy Z Flip 6 / 5 / 4 / 3 5G", + "Galaxy Note 20 Ultra 5G / Note 20 5G", + "Galaxy A56 5G / A55 5G / A54 5G / A53 5G", + "Galaxy A52s 5G / A51 5G / A35 5G / A23 5G", + "Galaxy M23 5G", + ], + }, + { + name: "Sony Xperia", + devices: [ + "Xperia 1 VI / 1 V / 1 IV / 1 III / 1 II", + "Xperia 5 V / 5 IV / 5 III / 5 II", + "Xperia 10 VI / 10 V / 10 IV / 10 III Lite", + "Xperia Pro-I / Pro", + "Xperia Ace III / Ace II / Ace", + "Xperia 8 Lite / XZ Premium", + "Xperia X Performance / Z5 Premium / Z5 Compact / Z5", + "Xperia Z4 / Z3 Compact / Z2 / Z", + ], + }, + { + name: "Sharp AQUOS", + devices: ["AQUOS R9 / R8 / R7", "AQUOS sense9 / sense8 / sense7", "AQUOS wish4 / wish3"], + }, + { + name: "Xiaomi / Redmi", + devices: [ + "Xiaomi 14T Pro / 14T / 14 Ultra / 14 Pro / 14 Pro+", + "Xiaomi 13T Pro / 13T / 13 Pro / 13 / 13 Lite", + "Xiaomi 12T Pro", + "Redmi Note 14 Pro / 13 Pro+ / 13 Pro 5G", + "Redmi Note 11 Pro 5G / 10T", + "Redmi 12 5G", + ], + }, + { + name: "Motorola", + devices: [ + "Edge 50 Ultra / 50s Pro / 50 Pro / 50 Neo / 50 Fusion", + "Edge 40 Pro / 40 Neo / 40", + "Edge+ (2024) / Edge+ (2023)", + "Razr 50 Ultra / 50 / 40 Ultra / 40", + "Razr 2024 / 2022 / 5G / 2019", + "Moto G85 / G64y 5G / G55 / G54 / G35", + "Moto G53J 5G / G52J 5G", + "ThinkPhone 25", + ], + }, + { + name: "OPPO / OnePlus", + devices: [ + "OPPO Find X8 / X5 Pro / X5 / X3 Pro", + "OPPO Find N2 Flip", + "OPPO Reno11 A / 10 Pro 5G / 9 A / 7 A", + "OPPO Reno6 Pro 5G / Reno 5 A / Reno A", + "OPPO A79 5G / A73 / A55s 5G / A3 5G", + "OnePlus 13 / 12 / 11", + ], + }, + { + name: "ASUS", + devices: [ + "Zenfone 9 / 8 Flip / 8", + "ROG Phone 7 / 6 / 5 / 3 / II", + "ZenFone 7 Pro / 7 / 6", + "ZenFone 5Z / 5 / 5Q", + "ZenFone 4 Series / 3 Series", + "ZenFone Max Series", + ], + }, + { + name: "Vivo / Nokia", + devices: ["Vivo X100 Pro / X90 Pro", "Vivo V40 / V29 / V29 Lite 5G", "Nokia XR21 / X30 / G60"], + }, + { + name: "HUAWEI", + devices: [ + "P40 Pro 5G / P40 / P40 lite 5G / P40 lite E", + "Mate 40 Pro+ / Mate 40 Pro / Mate 40", + "Mate 20 Pro / Mate 20 lite / Mate 10 Pro", + "P30 / P30 lite / P20 / P20 lite", + "nova 5T / nova lite 3+ / nova lite 3 / nova 3", + "MediaPad M5 / M3 / T5 Series", + ], + }, + { + name: "Fujitsu arrows", + devices: [ + "arrows We2 Plus / We2 / N", + "arrows NX9 F-52A", + "arrows M05 / M04 / M03 / M02", + "arrows SV F-03H / NX F-02H", + ], + }, + { + name: "Other Devices", + devices: [ + "DuraForce EX KY-51D / PRO", + "Kids Phones (Compact/KY-41C/SH-03M)", + "ASUS Chromebook CM30 Detachable", + "dtab Compact (d-52C/d-42A) / Standard (d-51C)", + "Essential Phone PH-1", + "HTC U12+ / U11 / U11 life", + "CAT S60 / S41 / S40", + "BlackBerry PRIV / Passport / Classic", + "Microsoft Surface Pro LTE / Surface 3 (4G)", + "Lenovo Tab4 8 / YOGA Series", + "LG Nexus 5X / Nexus 5", + ], + }, +]; + +// Flatten all devices for search +const ALL_DEVICES = DEVICE_CATEGORIES.flatMap(category => + category.devices.map(device => ({ + device, + category: category.name, + })) +); + +export function DeviceCompatibility() { + const [searchQuery, setSearchQuery] = useState(""); + const [isExpanded, setIsExpanded] = useState(false); + + const filteredDevices = useMemo(() => { + if (!searchQuery.trim()) return []; + + const query = searchQuery.toLowerCase(); + return ALL_DEVICES.filter( + item => + item.device.toLowerCase().includes(query) || item.category.toLowerCase().includes(query) + ).slice(0, 20); // Limit results for performance + }, [searchQuery]); + + const hasResults = filteredDevices.length > 0; + const showNoResults = searchQuery.trim().length > 0 && !hasResults; + + return ( +
+

Device Compatibility

+

+ Check if your device is compatible with our SIM service +

+ + {/* Search Box */} +
+
+ + setSearchQuery(e.target.value)} + placeholder="Search your device (e.g., iPhone 15, Galaxy S24, Pixel 8)" + className="w-full pl-12 pr-4 py-3 rounded-xl border border-border bg-card text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all" + /> +
+ + {/* Search Results */} + {searchQuery.trim() && ( +
+ {hasResults ? ( +
+ {filteredDevices.map((item, index) => ( +
+
+ +
+
+

{item.device}

+

{item.category}

+
+ + Compatible + +
+ ))} + {filteredDevices.length === 20 && ( +

+ Showing first 20 results. Try a more specific search. +

+ )} +
+ ) : showNoResults ? ( +
+
+ +
+

Device not found in our list

+

+ Your device may still be compatible. Please{" "} + + contact us + {" "} + to verify compatibility. +

+
+ ) : null} +
+ )} +
+ + {/* Expandable Full Device List */} +
+ + + {isExpanded && ( +
+
+

+ Below is a comprehensive list of devices confirmed to work with our SIM service. + Devices not listed may still be compatible. +

+
+
+ {DEVICE_CATEGORIES.map((category, catIndex) => ( + + ))} +
+
+ )} +
+
+ ); +} + +function DeviceCategorySection({ category }: { category: (typeof DEVICE_CATEGORIES)[number] }) { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ + {isOpen && ( +
+
+ {category.devices.map((device, index) => ( +
+ + {device} +
+ ))} +
+
+ )} +
+ ); +} + +export default DeviceCompatibility; diff --git a/apps/portal/src/features/services/components/sim/SimPlansContent.tsx b/apps/portal/src/features/services/components/sim/SimPlansContent.tsx index 142b1dd2..f1c2cc8b 100644 --- a/apps/portal/src/features/services/components/sim/SimPlansContent.tsx +++ b/apps/portal/src/features/services/components/sim/SimPlansContent.tsx @@ -26,6 +26,7 @@ import type { SimCatalogProduct } from "@customer-portal/domain/services"; import { ServicesBackLink } from "@/features/services/components/base/ServicesBackLink"; import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath"; import { CardPricing } from "@/features/services/components/base/CardPricing"; +import { DeviceCompatibility } from "./DeviceCompatibility"; import { ServiceHighlights, type HighlightFeature, @@ -613,6 +614,9 @@ export function SimPlansContent({ + {/* Device Compatibility Section */} + + {/* FAQ Section */}

@@ -620,24 +624,40 @@ export function SimPlansContent({

+ Enter "+" or "010", "recipient's country code", + and "recipient's phone number (regular phone number/mobile phone + number)" → Make a call. +
+
+ If the recipient's phone number begins with a 0, enter it without the first 0 + (except in some countries and regions). +
+ International calling rate is on the following Docomo's website:{" "} + + Docomo International Calling Rates + + + } /> -
@@ -654,7 +674,7 @@ export function SimPlansContent({ ); } -function FaqItem({ question, answer }: { question: string; answer: string }) { +function FaqItem({ question, answer }: { question: string; answer: React.ReactNode }) { const [isOpen, setIsOpen] = useState(false); return ( diff --git a/apps/portal/src/features/services/views/PublicInternetPlans.tsx b/apps/portal/src/features/services/views/PublicInternetPlans.tsx index ea2c2c43..a199e3fc 100644 --- a/apps/portal/src/features/services/views/PublicInternetPlans.tsx +++ b/apps/portal/src/features/services/views/PublicInternetPlans.tsx @@ -1,7 +1,17 @@ "use client"; -import { useMemo } from "react"; -import { ArrowRight, Sparkles, Wifi, Zap, Languages, FileText, Wrench, Globe } from "lucide-react"; +import { useMemo, useState } from "react"; +import { + ArrowRight, + Sparkles, + Wifi, + Zap, + Languages, + FileText, + Wrench, + Globe, + ChevronDown, +} from "lucide-react"; import { usePublicInternetCatalog } from "@/features/services/hooks"; import type { InternetInstallationCatalogItem, @@ -359,6 +369,9 @@ export function PublicInternetPlansContent({ {/* How It Works Section */} + {/* FAQ Section */} + + {/* Final CTA - Polished */}
@@ -421,3 +434,82 @@ function getTierFeatures(tier: string): string[] { }; return features[tier] ?? []; } + +// Internet FAQ data +const INTERNET_FAQ_ITEMS = [ + { + question: "How can I check if the 10Gbps service is available in my apartment or home?", + answer: + "Service availability depends on your address and building's network infrastructure. Please contact us (info@asolutions.co.jp) with your full address (including apartment name and room number) so we can confirm if our 10Gbps plans can be installed at your location.", + }, + { + question: + "My home requires multiple Wi-Fi routers for full residence coverage. Would you be able to assist with this?", + answer: + "Our tech consulting team will be able to make suggestions based on your residence layout and requirements. Please contact us at info@asolutions.co.jp for a free consultation.", + }, + { + question: + "We already have an Internet contract with a different company. Would it be possible to transfer our current service over to Assist Solutions?", + answer: + "Depending on the service that you are currently subscribed to, you may be able to transfer the service without cancelling. Please contact us at info@asolutions.co.jp for more information.", + }, + { + question: "What is the service contract period?", + answer: + "We offer our home Internet service on a monthly basis and thus, you will not be bound by yearly contracts. A designated cancellation form will need to be submitted by the 25th to close the account at the end of the month. For example, cancellation requests will need to be sent in by May 25th, in order to cancel at the end of May.", + }, + { + question: "How are invoices sent every month?", + answer: + "E-statements (available only in English) will be sent to your primary Email address. The service fee will be charged automatically to your registered credit card on file. For corporate plans, please contact us with your requests.", + }, + { + question: "My Internet is not working as expected. What should I do?", + answer: + "Our Chatbot will be able to assist you 24/7 with the initial diagnosis and offer troubleshooting tips based on your situation. You are also welcome to contact us at info@asolutions.co.jp for tech support (our business hours are 10AM-6PM/Mon-Sat). When contacting us, please be sure to let us know of the account holder's name and residence address so that we would be able to locate your account quicker.", + }, +]; + +/** + * Internet FAQ Section + */ +function InternetFaqSection() { + return ( +
+

+ Frequently Asked Questions +

+
+ {INTERNET_FAQ_ITEMS.map((item, index) => ( + + ))} +
+
+ ); +} + +/** + * FAQ Item component with expand/collapse functionality + */ +function FaqItem({ question, answer }: { question: string; answer: string }) { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ + {isOpen && ( +
{answer}
+ )} +
+ ); +} diff --git a/docs/integrations/salesforce/apex/SIMInventoryImporter.cls b/docs/integrations/salesforce/apex/SIMInventoryImporter.cls new file mode 100644 index 00000000..03f35d39 --- /dev/null +++ b/docs/integrations/salesforce/apex/SIMInventoryImporter.cls @@ -0,0 +1,271 @@ +/** + * SIMInventoryImporter + * Invocable Apex class for importing Physical SIM inventory from CSV files. + * Used by Screen Flow to allow employees to bulk import physical SIMs. + * + * CSV Format Expected (matching ASI_N6_PASI_*.csv): + * Row,Phone_Number,PT_Number,OEM_ID,Batch_Date,,,,, + * 1,02000002470001,PT0220024700010,PASI,20251229,,,, + * + * Note: No header row expected. All imports are Physical SIM type. + * + * @author Customer Portal Team + * @version 1.1 + */ +public with sharing class SIMInventoryImporter { + + // Hardcoded values for Physical SIM imports + private static final String SIM_TYPE = 'Physical SIM'; + private static final Boolean SKIP_HEADER = false; + + /** + * Input wrapper for the invocable method + */ + public class ImportRequest { + @InvocableVariable(label='Content Document IDs' description='IDs from File Upload component' required=true) + public List contentDocumentIds; + } + + /** + * Output wrapper for the invocable method + */ + public class ImportResult { + @InvocableVariable(label='Success') + public Boolean success; + + @InvocableVariable(label='Records Created') + public Integer recordsCreated; + + @InvocableVariable(label='Records Failed') + public Integer recordsFailed; + + @InvocableVariable(label='Error Messages') + public String errorMessages; + + @InvocableVariable(label='Summary Message') + public String summaryMessage; + } + + /** + * Main invocable method called by Flow + */ + @InvocableMethod(label='Import SIM Inventory from CSV' + description='Parses CSV content and creates SIM_Inventory__c records' + category='SIM Management') + public static List importFromCSV(List requests) { + List results = new List(); + + for (ImportRequest request : requests) { + results.add(processCSV(request)); + } + + return results; + } + + /** + * Process a single CSV import request + */ + private static ImportResult processCSV(ImportRequest request) { + ImportResult result = new ImportResult(); + result.success = true; + result.recordsCreated = 0; + result.recordsFailed = 0; + result.errorMessages = ''; + + try { + // Get the first Content Document ID from the list + if (request.contentDocumentIds == null || request.contentDocumentIds.isEmpty()) { + result.success = false; + result.errorMessages = 'No file was uploaded. Please select a CSV file.'; + result.summaryMessage = 'Import failed: No file uploaded'; + return result; + } + String contentDocumentId = request.contentDocumentIds[0]; + + // Retrieve file content from ContentVersion + List cvList = [ + SELECT VersionData + FROM ContentVersion + WHERE ContentDocumentId = :contentDocumentId + AND IsLatest = true + LIMIT 1 + ]; + + if (cvList.isEmpty()) { + result.success = false; + result.errorMessages = 'Could not find the uploaded file. Please try again.'; + result.summaryMessage = 'Import failed: File not found'; + return result; + } + + String csvContent = cvList[0].VersionData.toString(); + + // Parse CSV content + List lines = csvContent.split('\n'); + List simsToInsert = new List(); + List errors = new List(); + + // Start from first row (no header row in Physical SIM CSV files) + Integer startIndex = SKIP_HEADER ? 1 : 0; + + // Collect existing phone numbers to check for duplicates + Set existingPhoneNumbers = new Set(); + for (SIM_Inventory__c existing : [SELECT Phone_Number__c FROM SIM_Inventory__c WHERE Phone_Number__c != null]) { + existingPhoneNumbers.add(existing.Phone_Number__c); + } + + Set phoneNumbersInBatch = new Set(); + + for (Integer i = startIndex; i < lines.size(); i++) { + String line = lines[i].trim(); + + // Skip empty lines + if (String.isBlank(line)) { + continue; + } + + // Remove carriage return if present (Windows line endings) + line = line.replace('\r', ''); + + try { + // Parse CSV line + List columns = parseCSVLine(line); + + // Expected format: Row,Phone_Number,PT_Number,OEM_ID,Batch_Date,,,,, + if (columns.size() < 2) { + errors.add('Row ' + (i + 1) + ': Not enough columns (need at least phone number)'); + result.recordsFailed++; + continue; + } + + String phoneNumber = columns.size() > 1 ? columns[1].trim() : ''; + String ptNumber = columns.size() > 2 ? columns[2].trim() : ''; + String oemId = columns.size() > 3 ? columns[3].trim() : ''; + String batchDateStr = columns.size() > 4 ? columns[4].trim() : ''; + + // Validate phone number + if (String.isBlank(phoneNumber)) { + errors.add('Row ' + (i + 1) + ': Phone number is empty'); + result.recordsFailed++; + continue; + } + + // Check for duplicates in database + if (existingPhoneNumbers.contains(phoneNumber)) { + errors.add('Row ' + (i + 1) + ': Phone number ' + phoneNumber + ' already exists in database'); + result.recordsFailed++; + continue; + } + + // Check for duplicates within the CSV + if (phoneNumbersInBatch.contains(phoneNumber)) { + errors.add('Row ' + (i + 1) + ': Duplicate phone number ' + phoneNumber + ' in CSV file'); + result.recordsFailed++; + continue; + } + + // Parse batch date (format: YYYYMMDD) + Date batchDate = null; + if (String.isNotBlank(batchDateStr) && batchDateStr.length() >= 8) { + try { + Integer year = Integer.valueOf(batchDateStr.substring(0, 4)); + Integer month = Integer.valueOf(batchDateStr.substring(4, 6)); + Integer day = Integer.valueOf(batchDateStr.substring(6, 8)); + batchDate = Date.newInstance(year, month, day); + } catch (Exception e) { + // Leave as null if parsing fails - not critical + } + } + + // Create SIM_Inventory__c record + SIM_Inventory__c sim = new SIM_Inventory__c(); + sim.Phone_Number__c = phoneNumber; + sim.PT_Number__c = ptNumber; + sim.OEM_ID__c = oemId; + sim.Batch_Date__c = batchDate; + sim.Status__c = 'Available'; + sim.SIM_Type__c = SIM_TYPE; // Always Physical SIM + sim.Name = phoneNumber; // Use phone number as name for easy identification + + simsToInsert.add(sim); + phoneNumbersInBatch.add(phoneNumber); + + } catch (Exception e) { + errors.add('Row ' + (i + 1) + ': ' + e.getMessage()); + result.recordsFailed++; + } + } + + // Insert records with partial success allowed + if (!simsToInsert.isEmpty()) { + Database.SaveResult[] saveResults = Database.insert(simsToInsert, false); + + for (Integer i = 0; i < saveResults.size(); i++) { + if (saveResults[i].isSuccess()) { + result.recordsCreated++; + } else { + result.recordsFailed++; + for (Database.Error err : saveResults[i].getErrors()) { + errors.add('Insert error for ' + simsToInsert[i].Phone_Number__c + ': ' + err.getMessage()); + } + } + } + } + + // Build error message string (limit to first 10 errors for readability) + if (!errors.isEmpty()) { + if (errors.size() <= 10) { + result.errorMessages = String.join(errors, '\n'); + } else { + List firstTen = new List(); + for (Integer i = 0; i < 10; i++) { + firstTen.add(errors[i]); + } + result.errorMessages = String.join(firstTen, '\n') + + '\n\n... and ' + (errors.size() - 10) + ' more errors'; + } + } + + // Build summary message + result.summaryMessage = 'Import completed: ' + result.recordsCreated + ' records created successfully.'; + if (result.recordsFailed > 0) { + result.summaryMessage += ' ' + result.recordsFailed + ' records failed.'; + result.success = (result.recordsCreated > 0); // Partial success if any records created + } + + } catch (Exception e) { + result.success = false; + result.errorMessages = 'Critical error: ' + e.getMessage() + '\n\nStack trace: ' + e.getStackTraceString(); + result.summaryMessage = 'Import failed due to an unexpected error.'; + } + + return result; + } + + /** + * Parse a single CSV line, handling quoted fields properly + */ + private static List parseCSVLine(String line) { + List result = new List(); + Boolean inQuotes = false; + String currentField = ''; + + for (Integer i = 0; i < line.length(); i++) { + String c = line.substring(i, i + 1); + + if (c == '"') { + inQuotes = !inQuotes; + } else if (c == ',' && !inQuotes) { + result.add(currentField); + currentField = ''; + } else { + currentField += c; + } + } + + // Add the last field + result.add(currentField); + + return result; + } +} diff --git a/docs/integrations/salesforce/apex/SIMInventoryImporterTest.cls b/docs/integrations/salesforce/apex/SIMInventoryImporterTest.cls new file mode 100644 index 00000000..ade4a3f1 --- /dev/null +++ b/docs/integrations/salesforce/apex/SIMInventoryImporterTest.cls @@ -0,0 +1,209 @@ +/** + * Test class for SIMInventoryImporter + * Provides code coverage for deployment to production. + * + * @author Customer Portal Team + * @version 1.1 + */ +@isTest +private class SIMInventoryImporterTest { + + /** + * Helper method to create a ContentVersion (file) for testing + */ + private static String createTestFile(String csvContent) { + ContentVersion cv = new ContentVersion(); + cv.Title = 'Test SIM Import'; + cv.PathOnClient = 'test_sims.csv'; + cv.VersionData = Blob.valueOf(csvContent); + insert cv; + + // Get the ContentDocumentId + cv = [SELECT ContentDocumentId FROM ContentVersion WHERE Id = :cv.Id]; + return cv.ContentDocumentId; + } + + @isTest + static void testSuccessfulImport() { + // Prepare test CSV content (matches ASI_N6_PASI format - no header row) + String csvContent = '1,02000002470001,PT0220024700010,PASI,20251229,,,,\n' + + '2,02000002470002,PT0220024700020,PASI,20251229,,,,\n' + + '3,02000002470003,PT0220024700030,PASI,20251229,,,,'; + + String contentDocId = createTestFile(csvContent); + + SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest(); + request.contentDocumentIds = new List{ contentDocId }; + + Test.startTest(); + List results = + SIMInventoryImporter.importFromCSV(new List{ request }); + Test.stopTest(); + + System.assertEquals(1, results.size(), 'Should return one result'); + System.assertEquals(true, results[0].success, 'Import should succeed'); + System.assertEquals(3, results[0].recordsCreated, 'Should create 3 records'); + System.assertEquals(0, results[0].recordsFailed, 'Should have no failures'); + + // Verify records were created correctly + List sims = [ + SELECT Phone_Number__c, PT_Number__c, OEM_ID__c, Status__c, SIM_Type__c, Batch_Date__c + FROM SIM_Inventory__c + ORDER BY Phone_Number__c + ]; + System.assertEquals(3, sims.size(), 'Should have 3 SIM records'); + System.assertEquals('02000002470001', sims[0].Phone_Number__c); + System.assertEquals('PT0220024700010', sims[0].PT_Number__c); + System.assertEquals('PASI', sims[0].OEM_ID__c); + System.assertEquals('Available', sims[0].Status__c); + System.assertEquals('Physical SIM', sims[0].SIM_Type__c); + System.assertEquals(Date.newInstance(2025, 12, 29), sims[0].Batch_Date__c); + } + + @isTest + static void testDuplicateDetectionInDatabase() { + // Create existing record + insert new SIM_Inventory__c( + Name = '02000002470001', + Phone_Number__c = '02000002470001', + PT_Number__c = 'PT0220024700010', + Status__c = 'Available', + SIM_Type__c = 'Physical SIM' + ); + + // Try to import same phone number + String csvContent = '1,02000002470001,PT0220024700010,PASI,20251229,,,,'; + String contentDocId = createTestFile(csvContent); + + SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest(); + request.contentDocumentIds = new List{ contentDocId }; + + Test.startTest(); + List results = + SIMInventoryImporter.importFromCSV(new List{ request }); + Test.stopTest(); + + System.assertEquals(0, results[0].recordsCreated, 'Should not create duplicate'); + System.assertEquals(1, results[0].recordsFailed, 'Should report 1 failure'); + System.assert(results[0].errorMessages.contains('already exists'), 'Should mention duplicate'); + } + + @isTest + static void testDuplicateDetectionInCSV() { + // CSV with duplicate phone numbers + String csvContent = '1,02000002470001,PT0220024700010,PASI,20251229,,,,\n' + + '2,02000002470001,PT0220024700010,PASI,20251229,,,,'; + + String contentDocId = createTestFile(csvContent); + + SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest(); + request.contentDocumentIds = new List{ contentDocId }; + + Test.startTest(); + List results = + SIMInventoryImporter.importFromCSV(new List{ request }); + Test.stopTest(); + + System.assertEquals(1, results[0].recordsCreated, 'Should create first record only'); + System.assertEquals(1, results[0].recordsFailed, 'Should fail on duplicate'); + } + + @isTest + static void testEmptyPhoneNumber() { + String csvContent = '1,,PT0220024700010,PASI,20251229,,,,'; + String contentDocId = createTestFile(csvContent); + + SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest(); + request.contentDocumentIds = new List{ contentDocId }; + + Test.startTest(); + List results = + SIMInventoryImporter.importFromCSV(new List{ request }); + Test.stopTest(); + + System.assertEquals(0, results[0].recordsCreated, 'Should not create record without phone'); + System.assertEquals(1, results[0].recordsFailed, 'Should report failure'); + } + + @isTest + static void testEmptyFile() { + String csvContent = ''; + String contentDocId = createTestFile(csvContent); + + SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest(); + request.contentDocumentIds = new List{ contentDocId }; + + Test.startTest(); + List results = + SIMInventoryImporter.importFromCSV(new List{ request }); + Test.stopTest(); + + System.assertEquals(0, results[0].recordsCreated, 'Should create no records'); + System.assertEquals(0, results[0].recordsFailed, 'Should have no failures for empty file'); + } + + @isTest + static void testAlwaysPhysicalSIM() { + // Verify that all imported SIMs are set to Physical SIM type + String csvContent = '1,02000002470001,PT0220024700010,PASI,20251229,,,,'; + String contentDocId = createTestFile(csvContent); + + SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest(); + request.contentDocumentIds = new List{ contentDocId }; + + Test.startTest(); + List results = + SIMInventoryImporter.importFromCSV(new List{ request }); + Test.stopTest(); + + SIM_Inventory__c sim = [SELECT SIM_Type__c FROM SIM_Inventory__c LIMIT 1]; + System.assertEquals('Physical SIM', sim.SIM_Type__c, 'Should always be Physical SIM'); + } + + @isTest + static void testLargeImport() { + // Test with 50 records (matches real CSV file size) + String csvContent = ''; + for (Integer i = 1; i <= 50; i++) { + String phoneNum = '0200000247' + String.valueOf(i).leftPad(4, '0'); + String ptNum = 'PT022002470' + String.valueOf(i).leftPad(4, '0') + '0'; + csvContent += i + ',' + phoneNum + ',' + ptNum + ',PASI,20251229,,,,\n'; + } + + String contentDocId = createTestFile(csvContent); + + SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest(); + request.contentDocumentIds = new List{ contentDocId }; + + Test.startTest(); + List results = + SIMInventoryImporter.importFromCSV(new List{ request }); + Test.stopTest(); + + System.assertEquals(50, results[0].recordsCreated, 'Should create all 50 records'); + System.assertEquals(0, results[0].recordsFailed, 'Should have no failures'); + + Integer count = [SELECT COUNT() FROM SIM_Inventory__c]; + System.assertEquals(50, count, 'Database should have 50 records'); + } + + @isTest + static void testInvalidDateFormat() { + // Invalid date format should not fail the import, just leave date null + String csvContent = '1,02000002470001,PT0220024700010,PASI,invalid_date,,,,'; + String contentDocId = createTestFile(csvContent); + + SIMInventoryImporter.ImportRequest request = new SIMInventoryImporter.ImportRequest(); + request.contentDocumentIds = new List{ contentDocId }; + + Test.startTest(); + List results = + SIMInventoryImporter.importFromCSV(new List{ request }); + Test.stopTest(); + + System.assertEquals(1, results[0].recordsCreated, 'Should still create record'); + + SIM_Inventory__c sim = [SELECT Batch_Date__c FROM SIM_Inventory__c LIMIT 1]; + System.assertEquals(null, sim.Batch_Date__c, 'Date should be null for invalid format'); + } +} diff --git a/docs/integrations/salesforce/sim-inventory-import.md b/docs/integrations/salesforce/sim-inventory-import.md new file mode 100644 index 00000000..39f37f23 --- /dev/null +++ b/docs/integrations/salesforce/sim-inventory-import.md @@ -0,0 +1,256 @@ +# SIM Inventory CSV Import - Screen Flow Setup + +This guide provides the Apex class and Screen Flow configuration to enable employees to import Physical SIM data via CSV file upload. + +**Simplified for Physical SIM imports only - no header row expected.** + +--- + +## Overview + +The solution consists of: + +1. **Apex Invocable Class** - Parses CSV and creates SIM_Inventory\_\_c records +2. **Screen Flow** - Simple UI with just file upload and results display + +--- + +## Step 1: Deploy the Apex Classes + +Copy the Apex classes from: + +- `docs/integrations/salesforce/apex/SIMInventoryImporter.cls` +- `docs/integrations/salesforce/apex/SIMInventoryImporterTest.cls` + +### Deploy Steps: + +1. Go to **Setup → Apex Classes → New** +2. Paste the content of `SIMInventoryImporter.cls` → Save +3. Create another class, paste `SIMInventoryImporterTest.cls` → Save +4. Run tests to verify (Setup → Apex Test Execution) + +--- + +## Step 2: Create the Screen Flow + +### Flow Configuration + +1. Go to **Setup → Flows → New Flow** +2. Select **Screen Flow** +3. Click **Create** + +### Flow Elements + +#### Element 1: Screen - File Upload + +**Screen Properties:** + +- Label: `Upload Physical SIM CSV` +- API Name: `Upload_SIM_CSV` + +**Components on Screen:** + +1. **Display Text** (Header) + - API Name: `Header_Text` + - Content: + + ``` + # Import Physical SIM Inventory + + Upload a CSV file containing Physical SIM data. + + **Expected format (no header row):** + `Row,Phone_Number,PT_Number,OEM_ID,Batch_Date,,,,,` + + **Example:** + `1,02000002470001,PT0220024700010,PASI,20251229,,,,` + ``` + +2. **File Upload** + - API Name: `CSV_File_Upload` + - Label: `Select CSV File` + - Allow Multiple Files: `No` + - Accept: `.csv` + - Required: `Yes` + +#### Element 2: Action - Call Apex Importer + +**Action Properties:** + +- Category: `Apex` +- Action: `Import SIM Inventory from CSV` + +**Input Values:** + +- `contentDocumentId` → `{!CSV_File_Upload}` (the file upload component returns the ContentDocumentId) + +**Store Output:** + +- Create variables to store the output: + - `ImportResult_Success` (Boolean) + - `ImportResult_RecordsCreated` (Number) + - `ImportResult_RecordsFailed` (Number) + - `ImportResult_ErrorMessages` (Text) + - `ImportResult_SummaryMessage` (Text) + +#### Element 3: Screen - Results + +**Screen Properties:** + +- Label: `Import Results` +- API Name: `Import_Results` + +**Components:** + +1. **Display Text** (Success Message) + - API Name: `Success_Message` + - Visibility: Show when `{!ImportResult_Success} Equals true` + - Content: + + ``` + ✅ **Import Successful** + + **Records Created:** {!ImportResult_RecordsCreated} + + {!ImportResult_SummaryMessage} + ``` + +2. **Display Text** (Error Details) + - API Name: `Error_Details` + - Visibility: Show when `{!ImportResult_RecordsFailed} Greater than 0` + - Content: + + ``` + ⚠️ **Some records had issues:** + + {!ImportResult_ErrorMessages} + ``` + +3. **Display Text** (Failure Message) + - API Name: `Failure_Message` + - Visibility: Show when `{!ImportResult_Success} Equals false` + - Content: + + ``` + ❌ **Import Failed** + + {!ImportResult_ErrorMessages} + ``` + +--- + +## Step 3: Flow Diagram (Simplified) + +``` +┌─────────────────────────┐ +│ Start │ +└───────────┬─────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ Screen: Upload CSV │ +│ - File Upload only │ +└───────────┬─────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ Action: Import SIM │ +│ Inventory from CSV │ +│ (Apex Invocable) │ +└───────────┬─────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ Screen: Import Results │ +│ - Success/Fail Message │ +│ - Records Created │ +│ - Error Details │ +└───────────┬─────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ End │ +└─────────────────────────┘ +``` + +--- + +## Step 4: Add Flow to Lightning App + +1. Go to **Setup → App Manager** +2. Edit your app (e.g., "Sales" or custom app) +3. Add the Flow to utility items or create a Tab +4. Alternatively, embed in a Lightning Page: + - Edit any Lightning Record Page + - Add "Flow" component + - Select your "Import SIM Inventory" flow + +--- + +## Alternative: Quick Action Button + +Create a Quick Action to launch the flow from the SIM Inventory list view: + +1. **Setup → Object Manager → SIM Inventory → Buttons, Links, and Actions** +2. Click **New Action** +3. Action Type: `Flow` +4. Flow: Select your import flow +5. Label: `Import SIMs from CSV` +6. Add to Page Layout + +--- + +## CSV File Format Reference + +Your CSV files should follow this format: + +| Column | Field | Example | Required | +| ------ | ------------ | --------------- | ------------ | +| 1 | Row Number | 1 | No (ignored) | +| 2 | Phone Number | 02000002470001 | Yes | +| 3 | PT Number | PT0220024700010 | No | +| 4 | OEM ID | PASI | No | +| 5 | Batch Date | 20251229 | No | +| 6-9 | Empty | | No | + +**Example CSV:** + +```csv +1,02000002470001,PT0220024700010,PASI,20251229,,,, +2,02000002470002,PT0220024700020,PASI,20251229,,,, +3,02000002470003,PT0220024700030,PASI,20251229,,,, +``` + +--- + +## Troubleshooting + +### Common Issues + +1. **"Not enough columns" error** + - Ensure CSV has at least 5 columns (even if some are empty) + - Check for proper comma separators + +2. **"Phone number already exists" error** + - The phone number is already in SIM_Inventory\_\_c + - Check existing records before importing + +3. **File upload not working** + - Ensure file is .csv format + - Check file size (Salesforce limit: 25MB for files) + +4. **Permission errors** + - User needs Create permission on SIM_Inventory\_\_c + - User needs access to the Flow + +--- + +## Security Considerations + +- The Apex class uses `with sharing` to respect record-level security +- Only users with appropriate permissions can run the Flow +- Consider adding a Permission Set for SIM Inventory management + +--- + +**Last Updated:** January 2025