import { ConfigService } from "@nestjs/config"; import { EimsConfig } from "../../config/eims.config"; import { ETradeService } from "../companies/services/etrade.service"; import { eimsConfig, eimsInvoiceConfig } from "./eims-test-fixtures"; import { EimsSellerCacheService } from "./eims-seller-cache.service"; const registrationData = (over: Record = {}) => ({ companyName: "Ethio-Djibouti Railway PLC (eTrade)", region: "Addis Ababa", zone: "Bole", woreda: "Yeka", mobilePhone: "0911000000", regularPhone: "", ...over, }); const build = (cfg: EimsConfig = eimsConfig()) => { const resolveCompanyData = jest.fn(); const extractRegistrationData = jest.fn().mockReturnValue(registrationData()); const etrade = { resolveCompanyData, extractRegistrationData } as unknown as ETradeService; const config = { get: () => cfg } as unknown as ConfigService; const service = new EimsSellerCacheService(etrade, config); return { service, resolveCompanyData, extractRegistrationData, cfg }; }; const CODES = { buyerRegionCodes: { "Addis Ababa": "13" }, buyerWeredaCodes: { Yeka: "99" }, buyerCityCodes: { Bole: "101" }, }; describe("EimsSellerCacheService.getSellerDetails", () => { it("static config wins over a conflicting e-Trade value", async () => { const cfg = eimsConfig({ invoice: eimsInvoiceConfig({ sellerLegalName: "Ethio-Djibouti Railway S.C.", ...CODES }), }); const { service, resolveCompanyData } = build(cfg); resolveCompanyData.mockResolvedValue({ companyInfo: {}, businessInfo: {}, // presence is all that matters — extractRegistrationData is mocked }); await service.refresh(); const seller = service.getSellerDetails(cfg); // The static sellerLegalName ("Ethio-Djibouti Railway S.C.") must survive, not e-Trade's // differently-punctuated "Ethio-Djibouti Railway PLC (eTrade)". expect(seller.LegalName).toBe("Ethio-Djibouti Railway S.C."); }); it("e-Trade fills a field only when the static value is blank", async () => { const cfg = eimsConfig({ invoice: eimsInvoiceConfig({ sellerLegalName: "", sellerRegion: "", sellerWereda: "", sellerCity: null, ...CODES, }), }); const { service, resolveCompanyData } = build(cfg); resolveCompanyData.mockResolvedValue({ companyInfo: {}, businessInfo: {} }); await service.refresh(); const seller = service.getSellerDetails(cfg); expect(seller.LegalName).toBe("Ethio-Djibouti Railway PLC (eTrade)"); expect(seller.Region).toBe("13"); expect(seller.Wereda).toBe("99"); expect(seller.City).toBe("101"); }); it("VatNumber and Email are always the static value, never touched by e-Trade", async () => { const cfg = eimsConfig({ invoice: eimsInvoiceConfig({ sellerVatNumber: "0000000000", sellerEmail: "finance@example.et", ...CODES }), }); const { service, resolveCompanyData } = build(cfg); resolveCompanyData.mockResolvedValue({ companyInfo: {}, businessInfo: {} }); await service.refresh(); const seller = service.getSellerDetails(cfg); expect(seller.VatNumber).toBe("0000000000"); expect(seller.Email).toBe("finance@example.et"); }); it("falls back to the static config entirely when e-Trade has never been reachable", () => { const cfg = eimsConfig(); const { service } = build(cfg); // No refresh() ever called/succeeded — cached stays null. const seller = service.getSellerDetails(cfg); expect(seller.LegalName).toBe(cfg.invoice.sellerLegalName); expect(seller.Region).toBe(cfg.invoice.sellerRegion); }); it("does no I/O at all — filing never triggers an e-Trade request", () => { const { service, resolveCompanyData, cfg } = build(); service.getSellerDetails(cfg); service.getSellerDetails(cfg); expect(resolveCompanyData).not.toHaveBeenCalled(); }); }); describe("EimsSellerCacheService.refresh", () => { it("keeps the previous snapshot when a refresh fails", async () => { const cfg = eimsConfig({ invoice: eimsInvoiceConfig({ sellerLegalName: "", ...CODES }) }); const { service, resolveCompanyData } = build(cfg); resolveCompanyData.mockResolvedValueOnce({ companyInfo: {}, businessInfo: {} }); await service.refresh(); expect(service.getSellerDetails(cfg).LegalName).toBe("Ethio-Djibouti Railway PLC (eTrade)"); resolveCompanyData.mockRejectedValueOnce(new Error("eTrade down")); await service.refresh(); expect(service.getSellerDetails(cfg).LegalName).toBe("Ethio-Djibouti Railway PLC (eTrade)"); }); it("keeps the previous snapshot on timeout, without waiting for the slow request", async () => { jest.useFakeTimers(); try { const cfg = eimsConfig({ invoice: eimsInvoiceConfig({ sellerLegalName: "", ...CODES }) }); const { service, resolveCompanyData } = build(cfg); resolveCompanyData.mockResolvedValueOnce({ companyInfo: {}, businessInfo: {} }); await service.refresh(); expect(service.getSellerDetails(cfg).LegalName).toBe("Ethio-Djibouti Railway PLC (eTrade)"); resolveCompanyData.mockReturnValueOnce(new Promise(() => {})); // never resolves const refreshing = service.refresh(); await jest.advanceTimersByTimeAsync(10_000); await refreshing; expect(service.getSellerDetails(cfg).LegalName).toBe("Ethio-Djibouti Railway PLC (eTrade)"); } finally { jest.useRealTimers(); } }); it("does not start a second e-Trade request while one is already in flight", async () => { const { service, resolveCompanyData } = build(); let resolveCall: (value: unknown) => void = () => {}; resolveCompanyData.mockReturnValue(new Promise((resolve) => (resolveCall = resolve))); const first = service.refresh(); const second = service.refresh(); resolveCall({ companyInfo: {}, businessInfo: {} }); await Promise.all([first, second]); expect(resolveCompanyData).toHaveBeenCalledTimes(1); }); });