Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/amego-error-hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@paid-tw/einvoice-amego": minor
---

Add `amegoErrorHint()` — translate Amego account/setup-level raw error codes
(IP allowlist `14`, API access not enabled `22`, bad App Key `16`, account
disabled/suspended `13`/`19`, UBN mismatch `12`, transient `10`/`15`/`18`/`21`,
number tracks exhausted `3040111`/`3040191`) into actionable zh-TW guidance for
direct merchant display. Accepts a raw code (`"14"` / `14`) or an
`InvoiceError` (guarded via `isInvoiceError`, amego-only); returns `undefined`
for everything else so callers fall back to `error.message`.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"publint": "^0.3.21",
"tsdown": "^0.22.3",
"typescript": "^5.6.0",
"unrun": "^0.3.1",
"vite": "^8.1.0",
"vitest": "^4.1.9"
}
Expand Down
27 changes: 27 additions & 0 deletions packages/einvoice-amego/README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,33 @@ await invoices.validateBan("28080623"); // → boolean (company exists
capability) so the two providers are interchangeable; `barcodeQuery()` /
`banQuery()` remain for the full raw responses.

### Error hints (opt-in)

Amego's account/setup-level errors (IP allowlist, API access not enabled,
invoice-number tracks exhausted…) don't tell the merchant *what to do next*
— and they are exactly the ones only the merchant can fix in the Amego
backend. `amegoErrorHint()` translates those raw codes into actionable
zh-TW guidance suitable for direct display; anything else returns
`undefined` so you can fall back to `error.message` (Amego's original text):

```ts
import { amegoErrorHint } from "@paid-tw/einvoice-amego";
import { isInvoiceError } from "@paid-tw/einvoice";

try {
await invoices.issue(input);
} catch (e) {
const hint = amegoErrorHint(e); // also accepts a raw code: "14" / 14
showError(hint ?? (isInvoiceError(e) ? e.message : "issue failed"));
}
```

Covered: `12`/`13`/`14`/`16`/`19`/`22` (account & API-access setup),
`10`/`15`/`18`/`21` (transient Amego-side), `3040111`/`3040191` (number
tracks exhausted). Notably `14` ("IP 錯誤") is guaranteed to fire when the
Amego backend has an IP allowlist and requests come from cloud egress IPs —
the hint tells the merchant to remove the restriction.

## Config

| Option | Required | Description |
Expand Down
24 changes: 24 additions & 0 deletions packages/einvoice-amego/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,30 @@ await invoices.validateBan("28080623"); // → boolean (company exists
`validateMobileBarcode` 與 ezPay 轉接器一致(`CARRIER_VALIDATION` 能力),因此兩個
provider 可互換使用;`barcodeQuery()` / `banQuery()` 則保留以取得完整的原始回應。

### 錯誤提示(選用)

光貿的帳號/串接層錯誤(IP 限制、尚未申請 API 串接、字軌用罄…)從原始訊息看不出
「接下來該做什麼」,而這些恰好都需要商家自己到光貿後台處理。`amegoErrorHint()`
把這類錯誤碼翻成可直接顯示給商家的 zh-TW 行動指引;不屬於此類的錯誤回傳
`undefined`,請退回顯示 `error.message`(光貿原文):

```ts
import { amegoErrorHint } from "@paid-tw/einvoice-amego";
import { isInvoiceError } from "@paid-tw/einvoice";

try {
await invoices.issue(input);
} catch (e) {
const hint = amegoErrorHint(e); // 也接受 rawCode:"14" / 14
showError(hint ?? (isInvoiceError(e) ? e.message : "開立失敗"));
}
```

涵蓋:`12`/`13`/`14`/`16`/`19`/`22`(帳號與串接設定)、`10`/`15`/`18`/`21`
(光貿端暫時性錯誤)、`3040111`/`3040191`(字軌用罄)。其中 `14`「IP 錯誤」在
後台設有 IP 限制、而請求來自雲端(IP 不固定)時必然發生——提示會引導商家移除
IP 限制。

## 設定

| 選項 | 必填 | 說明 |
Expand Down
60 changes: 60 additions & 0 deletions packages/einvoice-amego/src/hints.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import { InvoiceError, InvoiceErrorCode } from "@paid-tw/einvoice";
import { amegoErrorHint } from "./hints.js";

function amegoError(rawCode: string, message = "boom"): InvoiceError {
return new InvoiceError(message, {
provider: "amego",
code: InvoiceErrorCode.AUTH,
rawCode,
});
}

describe("amegoErrorHint", () => {
it("maps account/setup-level codes to actionable zh-TW hints", () => {
expect(amegoErrorHint("14")).toContain("IP 限制");
expect(amegoErrorHint("22")).toContain("API 介接");
expect(amegoErrorHint("16")).toContain("App Key");
expect(amegoErrorHint("12")).toContain("統一編號");
expect(amegoErrorHint("19")).toContain("停權");
expect(amegoErrorHint("13")).toContain("尚未啟用");
});

it("accepts numeric raw codes", () => {
expect(amegoErrorHint(14)).toBe(amegoErrorHint("14"));
expect(amegoErrorHint(3040111)).toContain("字軌");
});

it("covers transient provider-side codes with a retry hint", () => {
for (const code of ["10", "15", "18", "21"]) {
expect(amegoErrorHint(code)).toContain("稍後再試");
}
});

it("extracts the raw code from an amego InvoiceError", () => {
expect(amegoErrorHint(amegoError("14", "IP 錯誤"))).toContain("IP 限制");
});

it("returns undefined for other providers' errors", () => {
const foreign = new InvoiceError("IP 錯誤", {
provider: "ecpay",
code: InvoiceErrorCode.AUTH,
rawCode: "14",
});
expect(amegoErrorHint(foreign)).toBeUndefined();
});

it("returns undefined for business errors the caller should handle in context", () => {
expect(amegoErrorHint("3040171")).toBeUndefined(); // OrderId 重複 (CONFLICT)
expect(amegoErrorHint("3050141")).toBeUndefined(); // 已存在折讓單
expect(amegoErrorHint("71")).toBeUndefined(); // 查無資料
});

it("returns undefined for non-errors and errors without a rawCode", () => {
expect(amegoErrorHint(undefined)).toBeUndefined();
expect(amegoErrorHint({})).toBeUndefined();
expect(amegoErrorHint(new Error("plain"))).toBeUndefined();
const noRaw = new InvoiceError("x", { provider: "amego", code: InvoiceErrorCode.NETWORK });
expect(amegoErrorHint(noRaw)).toBeUndefined();
});
});
65 changes: 65 additions & 0 deletions packages/einvoice-amego/src/hints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { isInvoiceError } from "@paid-tw/einvoice";

/**
* 商家可行動的錯誤提示(zh-TW)。
*
* Amego 的帳號/串接層錯誤(IP 限制、尚未申請 API 串接、字軌用罄…)從
* `InvoiceError` 的 `rawCode` + 原始 `message`(例如「IP 錯誤」)看不出
* 「接下來該做什麼」——而這幾種恰好都需要商家自己到光貿後台操作,接入方
* 無法以程式解決。此表把這些碼翻成可以直接顯示給商家的行動指引。
*
* 刻意只涵蓋「人需要介入」的碼:呼叫端程式錯誤(如參數格式)與一般業務
* 錯誤(發票不存在、金額超過…)不在此列——那些應該由接入方依自己的
* 業務語境處理。查表不中時回傳 `undefined`,呼叫端可退回顯示
* `error.message`(光貿原文)。
*
* 來源:光貿官方錯誤表(invoice.amego.tw/info_detail?mid=71);
* 14「IP 錯誤」的行為已對真實商家帳號實測驗證(2026-07)。
*/
const HINTS: Record<string, string> = {
// 通用錯誤 — 帳號 / 串接設定
"12": "統一編號與此光貿帳號不符,請確認填寫的是該帳號註冊的統編。",
"13": "光貿帳號尚未啟用,請聯繫光貿客服確認帳號狀態。",
"14": "來源 IP 遭光貿拒絕:後台「API 介接」設有 IP 限制。若請求來自雲端服務(IP 不固定),請至光貿後台移除 IP 限制。",
"16": "App Key 驗證失敗,請確認 App Key 是否貼錯,或已在光貿後台重新產生。",
"19": "光貿帳號已停權,請聯繫光貿客服。",
"22": "此光貿帳號尚未申請 API 串接,請至光貿後台「系統設定 → API 介接」申請並啟用。",

// 通用錯誤 — 暫時性(光貿端),稍後重試即可
"10": "光貿系統維護中,請稍後再試。",
"15": "請求時間驗證失敗(時鐘偏差),請稍後再試;若持續發生,可啟用 syncTime 設定。",
"18": "光貿系統忙碌(資料庫連線失敗),請稍後再試。",
"21": "光貿系統使用人數過多,請稍後再試。",

// 字軌用罄 — 需商家補號
"3040111": "發票字軌已用罄,無可用號碼。請至光貿後台補充本期字軌後,系統即可繼續開立。",
"3040191": "取號失敗(字軌配號異常),請至光貿後台確認本期字軌狀態。",
};

/**
* 依 Amego 原始錯誤碼取得商家行動指引。
*
* 接受原始碼(`rawCode`,數字或字串皆可)或直接丟入 `InvoiceError`
* (會先以 `isInvoiceError` 判別並確認 `provider === "amego"`)。
* 非商家可行動的錯誤回傳 `undefined`。
*
* ```ts
* try {
* await provider.issue(input);
* } catch (e) {
* const hint = amegoErrorHint(e);
* showError(hint ?? (isInvoiceError(e) ? e.message : "開立失敗"));
* }
* ```
*/
export function amegoErrorHint(rawCode: string | number): string | undefined;
export function amegoErrorHint(error: unknown): string | undefined;
export function amegoErrorHint(input: unknown): string | undefined {
if (typeof input === "string" || typeof input === "number") {
return HINTS[String(input)];
}
if (isInvoiceError(input) && input.provider === "amego" && input.rawCode !== undefined) {
return HINTS[input.rawCode];
}
return undefined;
}
1 change: 1 addition & 0 deletions packages/einvoice-amego/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export {
export type { TrackStatus, UploadStatus } from "./endpoints.js";
export { sign, mapAmegoErrorCode, clearTimeSyncCache, fetchServerTime } from "./client.js";
export type { AmegoResponse, AmegoTimeResponse } from "./client.js";
export { amegoErrorHint } from "./hints.js";
export {
amegoIssuePayloadSchema,
amegoCustomIssuePayloadSchema,
Expand Down
22 changes: 20 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.