@zentring/zinvoice 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +273 -0
- package/dist/index.cjs +3329 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2408 -0
- package/dist/index.d.ts +2408 -0
- package/dist/index.js +3274 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3274 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
|
|
3
|
+
// src/Provider.ts
|
|
4
|
+
var Provider = /* @__PURE__ */ ((Provider2) => {
|
|
5
|
+
Provider2["AMEGO"] = "amego";
|
|
6
|
+
return Provider2;
|
|
7
|
+
})(Provider || {});
|
|
8
|
+
var Capability = /* @__PURE__ */ ((Capability3) => {
|
|
9
|
+
Capability3["B2C"] = "b2c";
|
|
10
|
+
Capability3["B2B"] = "b2b";
|
|
11
|
+
Capability3["CARRIER"] = "carrier";
|
|
12
|
+
Capability3["DONATION"] = "donation";
|
|
13
|
+
Capability3["ALLOWANCE"] = "allowance";
|
|
14
|
+
Capability3["VOID"] = "void";
|
|
15
|
+
Capability3["EXCHANGE"] = "exchange";
|
|
16
|
+
Capability3["PRINT"] = "print";
|
|
17
|
+
Capability3["QUERY"] = "query";
|
|
18
|
+
Capability3["LIST"] = "list";
|
|
19
|
+
return Capability3;
|
|
20
|
+
})(Capability || {});
|
|
21
|
+
var PROVIDER_CAPABILITIES = {
|
|
22
|
+
["amego" /* AMEGO */]: /* @__PURE__ */ new Set([
|
|
23
|
+
"b2c" /* B2C */,
|
|
24
|
+
"b2b" /* B2B */,
|
|
25
|
+
"carrier" /* CARRIER */,
|
|
26
|
+
"donation" /* DONATION */,
|
|
27
|
+
"allowance" /* ALLOWANCE */,
|
|
28
|
+
"void" /* VOID */,
|
|
29
|
+
"exchange" /* EXCHANGE */,
|
|
30
|
+
"print" /* PRINT */,
|
|
31
|
+
"query" /* QUERY */,
|
|
32
|
+
"list" /* LIST */
|
|
33
|
+
])
|
|
34
|
+
};
|
|
35
|
+
var PROVIDER_NAMES = {
|
|
36
|
+
["amego" /* AMEGO */]: "\u5149\u8CBF\u8CC7\u8A0A"
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/errors/index.ts
|
|
40
|
+
var ZinvoiceError = class extends Error {
|
|
41
|
+
constructor(message) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "ZinvoiceError";
|
|
44
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
var InvalidTaxIdError = class extends ZinvoiceError {
|
|
48
|
+
constructor(value) {
|
|
49
|
+
super(`Invalid tax ID (\u7D71\u4E00\u7DE8\u865F): ${value}`);
|
|
50
|
+
this.value = value;
|
|
51
|
+
this.name = "InvalidTaxIdError";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var InvalidCarrierCodeError = class extends ZinvoiceError {
|
|
55
|
+
constructor(value, carrierType) {
|
|
56
|
+
super(`Invalid carrier code for type ${carrierType}: ${value}`);
|
|
57
|
+
this.value = value;
|
|
58
|
+
this.carrierType = carrierType;
|
|
59
|
+
this.name = "InvalidCarrierCodeError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var InvalidInvoiceNumberError = class extends ZinvoiceError {
|
|
63
|
+
constructor(value) {
|
|
64
|
+
super(`Invalid invoice number (\u767C\u7968\u865F\u78BC): ${value}`);
|
|
65
|
+
this.value = value;
|
|
66
|
+
this.name = "InvalidInvoiceNumberError";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var InvalidMoneyError = class extends ZinvoiceError {
|
|
70
|
+
constructor(value, reason) {
|
|
71
|
+
super(`Invalid money amount: ${value} - ${reason}`);
|
|
72
|
+
this.value = value;
|
|
73
|
+
this.name = "InvalidMoneyError";
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var ProviderApiError = class extends ZinvoiceError {
|
|
77
|
+
constructor(provider, code, message) {
|
|
78
|
+
super(`[${provider}] API Error (${code}): ${message}`);
|
|
79
|
+
this.provider = provider;
|
|
80
|
+
this.code = code;
|
|
81
|
+
this.name = "ProviderApiError";
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var NetworkError = class extends ZinvoiceError {
|
|
85
|
+
constructor(message, cause) {
|
|
86
|
+
super(`Network error: ${message}`);
|
|
87
|
+
this.cause = cause;
|
|
88
|
+
this.name = "NetworkError";
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var ValidationError = class extends ZinvoiceError {
|
|
92
|
+
constructor(field, message) {
|
|
93
|
+
super(`Validation error on ${field}: ${message}`);
|
|
94
|
+
this.field = field;
|
|
95
|
+
this.name = "ValidationError";
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
var UnsupportedCapabilityError = class extends ZinvoiceError {
|
|
99
|
+
constructor(capability, provider) {
|
|
100
|
+
super(`Provider "${provider}" does not support capability: ${capability}`);
|
|
101
|
+
this.capability = capability;
|
|
102
|
+
this.provider = provider;
|
|
103
|
+
this.name = "UnsupportedCapabilityError";
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var ProviderNotImplementedError = class extends ZinvoiceError {
|
|
107
|
+
constructor(provider) {
|
|
108
|
+
super(`Provider "${provider}" is not yet implemented`);
|
|
109
|
+
this.provider = provider;
|
|
110
|
+
this.name = "ProviderNotImplementedError";
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/infrastructure/amego/AmegoConfig.ts
|
|
115
|
+
var AMEGO_ENDPOINTS = {
|
|
116
|
+
// Invoice endpoints (發票)
|
|
117
|
+
INVOICE_ISSUE: "/json/f0401",
|
|
118
|
+
// 開立發票 (自動配號)
|
|
119
|
+
INVOICE_ISSUE_CUSTOM: "/json/f0401_custom",
|
|
120
|
+
// 開立發票 (API 配號)
|
|
121
|
+
INVOICE_VOID: "/json/f0501",
|
|
122
|
+
// 作廢發票
|
|
123
|
+
INVOICE_QUERY: "/json/invoice_query",
|
|
124
|
+
// 查詢單張發票
|
|
125
|
+
INVOICE_LIST: "/json/invoice_list",
|
|
126
|
+
// 查詢發票列表
|
|
127
|
+
INVOICE_STATUS: "/json/invoice_status",
|
|
128
|
+
// 查詢發票狀態
|
|
129
|
+
INVOICE_FILE: "/json/invoice_file",
|
|
130
|
+
// 下載發票檔案 (PDF)
|
|
131
|
+
INVOICE_PRINT: "/json/invoice_print",
|
|
132
|
+
// 產出列印格式字串
|
|
133
|
+
// Allowance endpoints (折讓)
|
|
134
|
+
ALLOWANCE_ISSUE: "/json/g0401",
|
|
135
|
+
// 開立折讓
|
|
136
|
+
ALLOWANCE_VOID: "/json/g0501",
|
|
137
|
+
// 作廢折讓
|
|
138
|
+
ALLOWANCE_QUERY: "/json/allowance_query",
|
|
139
|
+
// 查詢單張折讓
|
|
140
|
+
ALLOWANCE_LIST: "/json/allowance_list",
|
|
141
|
+
// 查詢折讓列表
|
|
142
|
+
ALLOWANCE_STATUS: "/json/allowance_status",
|
|
143
|
+
// 查詢折讓狀態
|
|
144
|
+
ALLOWANCE_FILE: "/json/allowance_file",
|
|
145
|
+
// 下載折讓檔案 (PDF)
|
|
146
|
+
ALLOWANCE_PRINT: "/json/allowance_print",
|
|
147
|
+
// 產出折讓列印格式字串
|
|
148
|
+
// Utility endpoints (其他)
|
|
149
|
+
BARCODE_CHECK: "/json/barcode",
|
|
150
|
+
// 手機條碼查詢
|
|
151
|
+
BAN_QUERY: "/json/ban_query",
|
|
152
|
+
// 公司名稱查詢
|
|
153
|
+
TRACK_ALL: "/json/track_all",
|
|
154
|
+
// 所有字軌資料
|
|
155
|
+
TRACK_GET: "/json/track_get",
|
|
156
|
+
// 字軌取號 (API 配號專用)
|
|
157
|
+
TRACK_STATUS: "/json/track_status",
|
|
158
|
+
// 字軌狀態 (API 配號專用)
|
|
159
|
+
LOTTERY_TYPE: "/json/lottery_type",
|
|
160
|
+
// 獎項定義
|
|
161
|
+
LOTTERY_STATUS: "/json/lottery_status"
|
|
162
|
+
// 中獎發票
|
|
163
|
+
};
|
|
164
|
+
var AMEGO_DEFAULTS = {
|
|
165
|
+
TIMEOUT: 3e4,
|
|
166
|
+
BASE_URL: "https://invoice-api.amego.tw"
|
|
167
|
+
};
|
|
168
|
+
var AmegoSigner = class {
|
|
169
|
+
constructor(apiKey) {
|
|
170
|
+
this.apiKey = apiKey;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Generate signature for API request
|
|
174
|
+
*
|
|
175
|
+
* @param data - Request data as JSON string
|
|
176
|
+
* @param timestamp - Unix timestamp in seconds
|
|
177
|
+
* @returns MD5 signature in lowercase hex
|
|
178
|
+
*/
|
|
179
|
+
sign(data, timestamp) {
|
|
180
|
+
const payload = `${data}${timestamp}${this.apiKey}`;
|
|
181
|
+
return createHash("md5").update(payload, "utf8").digest("hex").toLowerCase();
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Generate current timestamp (Unix seconds)
|
|
185
|
+
*/
|
|
186
|
+
static getTimestamp() {
|
|
187
|
+
return Math.floor(Date.now() / 1e3);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// src/infrastructure/amego/AmegoClient.ts
|
|
192
|
+
var AmegoClient = class {
|
|
193
|
+
baseUrl;
|
|
194
|
+
timeout;
|
|
195
|
+
signer;
|
|
196
|
+
sellerTaxId;
|
|
197
|
+
constructor(config) {
|
|
198
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
199
|
+
this.timeout = config.timeout ?? AMEGO_DEFAULTS.TIMEOUT;
|
|
200
|
+
this.signer = new AmegoSigner(config.apiKey);
|
|
201
|
+
this.sellerTaxId = config.sellerTaxId;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Send POST request to Amego API
|
|
205
|
+
*
|
|
206
|
+
* 請求格式:application/x-www-form-urlencoded
|
|
207
|
+
* 參數:
|
|
208
|
+
* - invoice: 統一編號
|
|
209
|
+
* - data: URL encoded JSON string
|
|
210
|
+
* - time: Unix timestamp
|
|
211
|
+
* - sign: MD5(data + time + apiKey)
|
|
212
|
+
*/
|
|
213
|
+
async post(endpoint, data) {
|
|
214
|
+
const timestamp = AmegoSigner.getTimestamp();
|
|
215
|
+
const jsonData = JSON.stringify(data);
|
|
216
|
+
const signature = this.signer.sign(jsonData, timestamp);
|
|
217
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
218
|
+
const formData = new URLSearchParams();
|
|
219
|
+
formData.append("invoice", this.sellerTaxId);
|
|
220
|
+
formData.append("data", jsonData);
|
|
221
|
+
formData.append("time", timestamp.toString());
|
|
222
|
+
formData.append("sign", signature);
|
|
223
|
+
try {
|
|
224
|
+
const controller = new AbortController();
|
|
225
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
226
|
+
const response = await fetch(url, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: {
|
|
229
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
230
|
+
},
|
|
231
|
+
body: formData.toString(),
|
|
232
|
+
signal: controller.signal
|
|
233
|
+
});
|
|
234
|
+
clearTimeout(timeoutId);
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
throw new ProviderApiError(
|
|
237
|
+
"amego",
|
|
238
|
+
`HTTP ${response.status}`,
|
|
239
|
+
`HTTP error: ${response.status} ${response.statusText}`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const result = await response.json();
|
|
243
|
+
if (result.code !== 0) {
|
|
244
|
+
throw new ProviderApiError(
|
|
245
|
+
"amego",
|
|
246
|
+
String(result.code),
|
|
247
|
+
result.msg || "Unknown error"
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error instanceof ProviderApiError) {
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
if (error instanceof Error) {
|
|
256
|
+
if (error.name === "AbortError") {
|
|
257
|
+
throw new NetworkError(`Request timeout after ${this.timeout}ms`);
|
|
258
|
+
}
|
|
259
|
+
throw new NetworkError(error.message);
|
|
260
|
+
}
|
|
261
|
+
throw new NetworkError("Unknown network error");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Send POST request and return raw response (for debugging)
|
|
266
|
+
*/
|
|
267
|
+
async postRaw(endpoint, data) {
|
|
268
|
+
const timestamp = AmegoSigner.getTimestamp();
|
|
269
|
+
const jsonData = JSON.stringify(data);
|
|
270
|
+
const signature = this.signer.sign(jsonData, timestamp);
|
|
271
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
272
|
+
const formData = new URLSearchParams();
|
|
273
|
+
formData.append("invoice", this.sellerTaxId);
|
|
274
|
+
formData.append("data", jsonData);
|
|
275
|
+
formData.append("time", timestamp.toString());
|
|
276
|
+
formData.append("sign", signature);
|
|
277
|
+
const controller = new AbortController();
|
|
278
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
279
|
+
const response = await fetch(url, {
|
|
280
|
+
method: "POST",
|
|
281
|
+
headers: {
|
|
282
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
283
|
+
},
|
|
284
|
+
body: formData.toString(),
|
|
285
|
+
signal: controller.signal
|
|
286
|
+
});
|
|
287
|
+
clearTimeout(timeoutId);
|
|
288
|
+
const headers = {};
|
|
289
|
+
response.headers.forEach((value, key) => {
|
|
290
|
+
headers[key] = value;
|
|
291
|
+
});
|
|
292
|
+
const body = await response.text();
|
|
293
|
+
let parsed;
|
|
294
|
+
try {
|
|
295
|
+
parsed = JSON.parse(body);
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
status: response.status,
|
|
300
|
+
statusText: response.statusText,
|
|
301
|
+
headers,
|
|
302
|
+
body,
|
|
303
|
+
parsed
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// src/domain/shared/Money.ts
|
|
309
|
+
var Money = class _Money {
|
|
310
|
+
constructor(amount) {
|
|
311
|
+
this.amount = amount;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Create Money from a number
|
|
315
|
+
* @throws InvalidMoneyError if the amount is invalid
|
|
316
|
+
*/
|
|
317
|
+
static create(amount) {
|
|
318
|
+
if (!Number.isFinite(amount)) {
|
|
319
|
+
throw new InvalidMoneyError(amount, "Amount must be a finite number");
|
|
320
|
+
}
|
|
321
|
+
return new _Money(amount);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Create Money representing zero
|
|
325
|
+
*/
|
|
326
|
+
static zero() {
|
|
327
|
+
return new _Money(0);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Create Money from cents (integer)
|
|
331
|
+
*/
|
|
332
|
+
static fromCents(cents) {
|
|
333
|
+
if (!Number.isInteger(cents)) {
|
|
334
|
+
throw new InvalidMoneyError(cents, "Cents must be an integer");
|
|
335
|
+
}
|
|
336
|
+
return new _Money(cents / 100);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Get the numeric value
|
|
340
|
+
*/
|
|
341
|
+
toNumber() {
|
|
342
|
+
return this.amount;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Get value rounded to integer
|
|
346
|
+
*/
|
|
347
|
+
toInteger() {
|
|
348
|
+
return Math.round(this.amount);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Get value in cents (integer)
|
|
352
|
+
*/
|
|
353
|
+
toCents() {
|
|
354
|
+
return Math.round(this.amount * 100);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Add another Money value
|
|
358
|
+
*/
|
|
359
|
+
add(other) {
|
|
360
|
+
return new _Money(this.amount + other.amount);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Subtract another Money value
|
|
364
|
+
*/
|
|
365
|
+
subtract(other) {
|
|
366
|
+
return new _Money(this.amount - other.amount);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Multiply by a factor
|
|
370
|
+
*/
|
|
371
|
+
multiply(factor) {
|
|
372
|
+
return new _Money(this.amount * factor);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Divide by a divisor
|
|
376
|
+
*/
|
|
377
|
+
divide(divisor) {
|
|
378
|
+
if (divisor === 0) {
|
|
379
|
+
throw new InvalidMoneyError(this.amount, "Cannot divide by zero");
|
|
380
|
+
}
|
|
381
|
+
return new _Money(this.amount / divisor);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Check if this amount is zero
|
|
385
|
+
*/
|
|
386
|
+
isZero() {
|
|
387
|
+
return this.amount === 0;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Check if this amount is positive
|
|
391
|
+
*/
|
|
392
|
+
isPositive() {
|
|
393
|
+
return this.amount > 0;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Check if this amount is negative
|
|
397
|
+
*/
|
|
398
|
+
isNegative() {
|
|
399
|
+
return this.amount < 0;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Get absolute value
|
|
403
|
+
*/
|
|
404
|
+
abs() {
|
|
405
|
+
return new _Money(Math.abs(this.amount));
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Round to specified decimal places
|
|
409
|
+
*/
|
|
410
|
+
round(decimals = 0) {
|
|
411
|
+
const factor = Math.pow(10, decimals);
|
|
412
|
+
return new _Money(Math.round(this.amount * factor) / factor);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Format as string with specified decimal places
|
|
416
|
+
*/
|
|
417
|
+
format(decimals = 2) {
|
|
418
|
+
return this.amount.toFixed(decimals);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Check equality with another Money
|
|
422
|
+
*/
|
|
423
|
+
equals(other) {
|
|
424
|
+
return this.amount === other.amount;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Compare with another Money
|
|
428
|
+
* Returns: -1 if less, 0 if equal, 1 if greater
|
|
429
|
+
*/
|
|
430
|
+
compareTo(other) {
|
|
431
|
+
if (this.amount < other.amount) return -1;
|
|
432
|
+
if (this.amount > other.amount) return 1;
|
|
433
|
+
return 0;
|
|
434
|
+
}
|
|
435
|
+
toString() {
|
|
436
|
+
return this.amount.toString();
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// src/domain/shared/Carrier.ts
|
|
441
|
+
var Carrier = class _Carrier {
|
|
442
|
+
constructor(_type, _id) {
|
|
443
|
+
this._type = _type;
|
|
444
|
+
this._id = _id;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Create a "no carrier" instance
|
|
448
|
+
*/
|
|
449
|
+
static none() {
|
|
450
|
+
return new _Carrier("" /* NONE */, "");
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Create a mobile barcode carrier (手機條碼)
|
|
454
|
+
*
|
|
455
|
+
* @param barcode The mobile barcode (format: /XXXXXXX)
|
|
456
|
+
*/
|
|
457
|
+
static mobile(barcode) {
|
|
458
|
+
const trimmed = barcode?.trim();
|
|
459
|
+
if (!trimmed) {
|
|
460
|
+
throw new ValidationError("carrier", "Mobile barcode is required");
|
|
461
|
+
}
|
|
462
|
+
const pattern = /^\/[0-9A-Z.+-]{7}$/;
|
|
463
|
+
if (!pattern.test(trimmed.toUpperCase())) {
|
|
464
|
+
throw new ValidationError(
|
|
465
|
+
"carrier",
|
|
466
|
+
"Invalid mobile barcode format. Expected: /XXXXXXX"
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return new _Carrier("3J0002" /* MOBILE */, trimmed.toUpperCase());
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Create a certificate carrier (自然人憑證)
|
|
473
|
+
*
|
|
474
|
+
* @param certId The certificate ID (16 characters)
|
|
475
|
+
*/
|
|
476
|
+
static certificate(certId) {
|
|
477
|
+
const trimmed = certId?.trim();
|
|
478
|
+
if (!trimmed) {
|
|
479
|
+
throw new ValidationError("carrier", "Certificate ID is required");
|
|
480
|
+
}
|
|
481
|
+
if (!/^[A-Z0-9]{16}$/i.test(trimmed)) {
|
|
482
|
+
throw new ValidationError(
|
|
483
|
+
"carrier",
|
|
484
|
+
"Invalid certificate ID format. Expected: 16 alphanumeric characters"
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
return new _Carrier("CQ0001" /* CERTIFICATE */, trimmed.toUpperCase());
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Create a custom carrier
|
|
491
|
+
*/
|
|
492
|
+
static custom(type, id) {
|
|
493
|
+
if (type === "" /* NONE */) {
|
|
494
|
+
return _Carrier.none();
|
|
495
|
+
}
|
|
496
|
+
return new _Carrier(type, id?.trim() ?? "");
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Get the carrier type
|
|
500
|
+
*/
|
|
501
|
+
get type() {
|
|
502
|
+
return this._type;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Get the carrier type code (for API)
|
|
506
|
+
*/
|
|
507
|
+
get typeCode() {
|
|
508
|
+
return this._type;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Get the carrier ID
|
|
512
|
+
*/
|
|
513
|
+
get id() {
|
|
514
|
+
return this._id;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Check if this is an empty carrier
|
|
518
|
+
*/
|
|
519
|
+
get isEmpty() {
|
|
520
|
+
return this._type === "" /* NONE */;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Check if this is a mobile barcode
|
|
524
|
+
*/
|
|
525
|
+
get isMobile() {
|
|
526
|
+
return this._type === "3J0002" /* MOBILE */;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Check if this is a certificate
|
|
530
|
+
*/
|
|
531
|
+
get isCertificate() {
|
|
532
|
+
return this._type === "CQ0001" /* CERTIFICATE */;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// src/domain/shared/Donation.ts
|
|
537
|
+
var Donation = class _Donation {
|
|
538
|
+
constructor(_code) {
|
|
539
|
+
this._code = _code;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Create a "no donation" instance
|
|
543
|
+
*/
|
|
544
|
+
static none() {
|
|
545
|
+
return new _Donation("");
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Create a donation with code (愛心碼)
|
|
549
|
+
*
|
|
550
|
+
* @param code The donation code (3-7 digits)
|
|
551
|
+
*/
|
|
552
|
+
static code(code) {
|
|
553
|
+
const trimmed = code?.trim();
|
|
554
|
+
if (!trimmed) {
|
|
555
|
+
throw new ValidationError("donation", "Donation code is required");
|
|
556
|
+
}
|
|
557
|
+
if (!/^\d{3,7}$/.test(trimmed)) {
|
|
558
|
+
throw new ValidationError(
|
|
559
|
+
"donation",
|
|
560
|
+
"Invalid donation code format. Expected: 3-7 digits"
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
return new _Donation(trimmed);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Try to create a Donation, returns none() if invalid
|
|
567
|
+
*/
|
|
568
|
+
static tryCreate(code) {
|
|
569
|
+
try {
|
|
570
|
+
return _Donation.code(code);
|
|
571
|
+
} catch {
|
|
572
|
+
return _Donation.none();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Get the donation code
|
|
577
|
+
*/
|
|
578
|
+
get code() {
|
|
579
|
+
return this._code;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Check if this is an empty donation (not donating)
|
|
583
|
+
*/
|
|
584
|
+
get isEmpty() {
|
|
585
|
+
return this._code.length === 0;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Check if this invoice is being donated
|
|
589
|
+
*/
|
|
590
|
+
get isDonating() {
|
|
591
|
+
return this._code.length > 0;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Get the string value
|
|
595
|
+
*/
|
|
596
|
+
toString() {
|
|
597
|
+
return this._code;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// src/domain/shared/TaxType.ts
|
|
602
|
+
var TaxType = /* @__PURE__ */ ((TaxType2) => {
|
|
603
|
+
TaxType2[TaxType2["TAXABLE"] = 1] = "TAXABLE";
|
|
604
|
+
TaxType2[TaxType2["ZERO_RATED"] = 2] = "ZERO_RATED";
|
|
605
|
+
TaxType2[TaxType2["TAX_EXEMPT"] = 3] = "TAX_EXEMPT";
|
|
606
|
+
TaxType2[TaxType2["SPECIAL_TAX"] = 4] = "SPECIAL_TAX";
|
|
607
|
+
TaxType2[TaxType2["MIXED"] = 9] = "MIXED";
|
|
608
|
+
return TaxType2;
|
|
609
|
+
})(TaxType || {});
|
|
610
|
+
var ZeroTaxRateReason = /* @__PURE__ */ ((ZeroTaxRateReason3) => {
|
|
611
|
+
ZeroTaxRateReason3[ZeroTaxRateReason3["EXPORT_GOODS"] = 71] = "EXPORT_GOODS";
|
|
612
|
+
ZeroTaxRateReason3[ZeroTaxRateReason3["EXPORT_SERVICES"] = 72] = "EXPORT_SERVICES";
|
|
613
|
+
ZeroTaxRateReason3[ZeroTaxRateReason3["DUTY_FREE_SHOP"] = 73] = "DUTY_FREE_SHOP";
|
|
614
|
+
ZeroTaxRateReason3[ZeroTaxRateReason3["BONDED_AREA"] = 74] = "BONDED_AREA";
|
|
615
|
+
ZeroTaxRateReason3[ZeroTaxRateReason3["INTERNATIONAL_TRANSPORT"] = 75] = "INTERNATIONAL_TRANSPORT";
|
|
616
|
+
ZeroTaxRateReason3[ZeroTaxRateReason3["INTERNATIONAL_VESSELS"] = 76] = "INTERNATIONAL_VESSELS";
|
|
617
|
+
ZeroTaxRateReason3[ZeroTaxRateReason3["VESSEL_SUPPLIES"] = 77] = "VESSEL_SUPPLIES";
|
|
618
|
+
ZeroTaxRateReason3[ZeroTaxRateReason3["BONDED_DIRECT_EXPORT"] = 78] = "BONDED_DIRECT_EXPORT";
|
|
619
|
+
ZeroTaxRateReason3[ZeroTaxRateReason3["BONDED_WAREHOUSE"] = 79] = "BONDED_WAREHOUSE";
|
|
620
|
+
return ZeroTaxRateReason3;
|
|
621
|
+
})(ZeroTaxRateReason || {});
|
|
622
|
+
var CustomsClearanceMark = /* @__PURE__ */ ((CustomsClearanceMark3) => {
|
|
623
|
+
CustomsClearanceMark3[CustomsClearanceMark3["NON_CUSTOMS"] = 1] = "NON_CUSTOMS";
|
|
624
|
+
CustomsClearanceMark3[CustomsClearanceMark3["CUSTOMS"] = 2] = "CUSTOMS";
|
|
625
|
+
return CustomsClearanceMark3;
|
|
626
|
+
})(CustomsClearanceMark || {});
|
|
627
|
+
var TAX_RATE = 0.05;
|
|
628
|
+
|
|
629
|
+
// src/domain/invoice/Invoice.ts
|
|
630
|
+
var InvoiceStatus = /* @__PURE__ */ ((InvoiceStatus2) => {
|
|
631
|
+
InvoiceStatus2[InvoiceStatus2["PENDING"] = 1] = "PENDING";
|
|
632
|
+
InvoiceStatus2[InvoiceStatus2["UPLOADING"] = 2] = "UPLOADING";
|
|
633
|
+
InvoiceStatus2[InvoiceStatus2["UPLOADED"] = 3] = "UPLOADED";
|
|
634
|
+
InvoiceStatus2[InvoiceStatus2["PROCESSING"] = 31] = "PROCESSING";
|
|
635
|
+
InvoiceStatus2[InvoiceStatus2["AWAITING_CONFIRMATION"] = 32] = "AWAITING_CONFIRMATION";
|
|
636
|
+
InvoiceStatus2[InvoiceStatus2["ERROR"] = 91] = "ERROR";
|
|
637
|
+
InvoiceStatus2[InvoiceStatus2["COMPLETED"] = 99] = "COMPLETED";
|
|
638
|
+
return InvoiceStatus2;
|
|
639
|
+
})(InvoiceStatus || {});
|
|
640
|
+
var InvoiceType = /* @__PURE__ */ ((InvoiceType2) => {
|
|
641
|
+
InvoiceType2["B2C_ISSUE"] = "C0401";
|
|
642
|
+
InvoiceType2["B2C_VOID"] = "C0501";
|
|
643
|
+
InvoiceType2["B2C_CANCEL"] = "C0701";
|
|
644
|
+
InvoiceType2["B2B_ISSUE"] = "A0401";
|
|
645
|
+
InvoiceType2["B2B_VOID"] = "A0501";
|
|
646
|
+
return InvoiceType2;
|
|
647
|
+
})(InvoiceType || {});
|
|
648
|
+
var Invoice = class _Invoice {
|
|
649
|
+
constructor(_orderId, _buyer, _items, _carrier, _donation, _remark, _trackApiCode, _taxType, _customsClearanceMark, _zeroTaxRateReason, _brandName, _pricesIncludeTax = true, _status = 1 /* PENDING */, _type = "C0401" /* B2C_ISSUE */) {
|
|
650
|
+
this._orderId = _orderId;
|
|
651
|
+
this._buyer = _buyer;
|
|
652
|
+
this._items = _items;
|
|
653
|
+
this._carrier = _carrier;
|
|
654
|
+
this._donation = _donation;
|
|
655
|
+
this._remark = _remark;
|
|
656
|
+
this._trackApiCode = _trackApiCode;
|
|
657
|
+
this._taxType = _taxType;
|
|
658
|
+
this._customsClearanceMark = _customsClearanceMark;
|
|
659
|
+
this._zeroTaxRateReason = _zeroTaxRateReason;
|
|
660
|
+
this._brandName = _brandName;
|
|
661
|
+
this._pricesIncludeTax = _pricesIncludeTax;
|
|
662
|
+
this._status = _status;
|
|
663
|
+
this._type = _type;
|
|
664
|
+
}
|
|
665
|
+
_invoiceNumber;
|
|
666
|
+
_invoiceTime;
|
|
667
|
+
_randomNumber;
|
|
668
|
+
/**
|
|
669
|
+
* Create a new Invoice
|
|
670
|
+
*/
|
|
671
|
+
static create(props) {
|
|
672
|
+
if (!props.items || props.items.length === 0) {
|
|
673
|
+
throw new ValidationError("items", "At least one item is required");
|
|
674
|
+
}
|
|
675
|
+
if (props.items.length > 9999) {
|
|
676
|
+
throw new ValidationError("items", "Maximum 9999 items allowed");
|
|
677
|
+
}
|
|
678
|
+
if (props.remark && props.remark.length > 200) {
|
|
679
|
+
throw new ValidationError(
|
|
680
|
+
"remark",
|
|
681
|
+
"Remark must not exceed 200 characters"
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
const taxType = _Invoice.determineTaxType(props.items);
|
|
685
|
+
if (taxType === 2 /* ZERO_RATED */) {
|
|
686
|
+
if (!props.customsClearanceMark) {
|
|
687
|
+
throw new ValidationError(
|
|
688
|
+
"customsClearanceMark",
|
|
689
|
+
"Customs clearance mark is required for zero-rated invoices"
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
if (!props.zeroTaxRateReason) {
|
|
693
|
+
throw new ValidationError(
|
|
694
|
+
"zeroTaxRateReason",
|
|
695
|
+
"Zero tax rate reason is required for zero-rated invoices"
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const hasCarrier = props.carrier && !props.carrier.isEmpty;
|
|
700
|
+
const hasDonation = props.donation && props.donation.isDonating;
|
|
701
|
+
if (hasCarrier && hasDonation) {
|
|
702
|
+
throw new ValidationError(
|
|
703
|
+
"carrier",
|
|
704
|
+
"Cannot have both carrier and donation on the same invoice"
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
const isB2B = props.buyer.isCompany;
|
|
708
|
+
return new _Invoice(
|
|
709
|
+
props.orderId,
|
|
710
|
+
props.buyer,
|
|
711
|
+
props.items,
|
|
712
|
+
props.carrier ?? Carrier.none(),
|
|
713
|
+
props.donation ?? Donation.none(),
|
|
714
|
+
props.remark?.trim() ?? "",
|
|
715
|
+
props.trackApiCode?.trim() ?? "",
|
|
716
|
+
taxType,
|
|
717
|
+
props.customsClearanceMark,
|
|
718
|
+
props.zeroTaxRateReason,
|
|
719
|
+
props.brandName?.trim(),
|
|
720
|
+
props.pricesIncludeTax ?? true,
|
|
721
|
+
1 /* PENDING */,
|
|
722
|
+
isB2B ? "A0401" /* B2B_ISSUE */ : "C0401" /* B2C_ISSUE */
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Determine overall tax type from items
|
|
727
|
+
*/
|
|
728
|
+
static determineTaxType(items) {
|
|
729
|
+
const taxTypes = new Set(items.map((item) => item.taxType));
|
|
730
|
+
if (taxTypes.size === 1) {
|
|
731
|
+
return items[0].taxType;
|
|
732
|
+
}
|
|
733
|
+
return 9 /* MIXED */;
|
|
734
|
+
}
|
|
735
|
+
// --- Calculated amounts ---
|
|
736
|
+
/**
|
|
737
|
+
* Calculate sales amount (應稅銷售額)
|
|
738
|
+
*/
|
|
739
|
+
calculateSalesAmount() {
|
|
740
|
+
return this._items.filter((item) => item.taxType === 1 /* TAXABLE */).reduce((sum, item) => sum.add(item.amount), Money.zero()).round();
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Calculate free tax sales amount (免稅銷售額)
|
|
744
|
+
*/
|
|
745
|
+
calculateFreeTaxSalesAmount() {
|
|
746
|
+
return this._items.filter((item) => item.taxType === 3 /* TAX_EXEMPT */).reduce((sum, item) => sum.add(item.amount), Money.zero()).round();
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Calculate zero tax sales amount (零稅率銷售額)
|
|
750
|
+
*/
|
|
751
|
+
calculateZeroTaxSalesAmount() {
|
|
752
|
+
return this._items.filter((item) => item.taxType === 2 /* ZERO_RATED */).reduce((sum, item) => sum.add(item.amount), Money.zero()).round();
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Calculate tax amount (營業稅額)
|
|
756
|
+
*/
|
|
757
|
+
calculateTaxAmount() {
|
|
758
|
+
if (this._buyer.isAnonymous) {
|
|
759
|
+
return Money.zero();
|
|
760
|
+
}
|
|
761
|
+
const salesAmount = this.calculateSalesAmount();
|
|
762
|
+
if (this._pricesIncludeTax) {
|
|
763
|
+
const beforeTax = salesAmount.divide(1.05).round();
|
|
764
|
+
return salesAmount.subtract(beforeTax);
|
|
765
|
+
} else {
|
|
766
|
+
return salesAmount.multiply(0.05).round();
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Calculate total amount (總計)
|
|
771
|
+
*/
|
|
772
|
+
calculateTotalAmount() {
|
|
773
|
+
const salesAmount = this.calculateSalesAmount();
|
|
774
|
+
const freeTaxAmount = this.calculateFreeTaxSalesAmount();
|
|
775
|
+
const zeroTaxAmount = this.calculateZeroTaxSalesAmount();
|
|
776
|
+
const taxAmount = this.calculateTaxAmount();
|
|
777
|
+
if (this._pricesIncludeTax) {
|
|
778
|
+
return salesAmount.add(freeTaxAmount).add(zeroTaxAmount);
|
|
779
|
+
} else {
|
|
780
|
+
return salesAmount.add(freeTaxAmount).add(zeroTaxAmount).add(taxAmount);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
// --- State mutations ---
|
|
784
|
+
/**
|
|
785
|
+
* Set invoice number after issuing
|
|
786
|
+
*/
|
|
787
|
+
setInvoiceNumber(number, time, randomNumber) {
|
|
788
|
+
this._invoiceNumber = number;
|
|
789
|
+
this._invoiceTime = time;
|
|
790
|
+
this._randomNumber = randomNumber;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Update status
|
|
794
|
+
*/
|
|
795
|
+
updateStatus(status) {
|
|
796
|
+
this._status = status;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Mark as voided
|
|
800
|
+
*/
|
|
801
|
+
markAsVoided() {
|
|
802
|
+
this._type = this.isB2B ? "A0501" /* B2B_VOID */ : "C0501" /* B2C_VOID */;
|
|
803
|
+
}
|
|
804
|
+
// --- Getters ---
|
|
805
|
+
get orderId() {
|
|
806
|
+
return this._orderId;
|
|
807
|
+
}
|
|
808
|
+
get invoiceNumber() {
|
|
809
|
+
return this._invoiceNumber;
|
|
810
|
+
}
|
|
811
|
+
get invoiceTime() {
|
|
812
|
+
return this._invoiceTime;
|
|
813
|
+
}
|
|
814
|
+
get randomNumber() {
|
|
815
|
+
return this._randomNumber;
|
|
816
|
+
}
|
|
817
|
+
get buyer() {
|
|
818
|
+
return this._buyer;
|
|
819
|
+
}
|
|
820
|
+
get carrier() {
|
|
821
|
+
return this._carrier;
|
|
822
|
+
}
|
|
823
|
+
get donation() {
|
|
824
|
+
return this._donation;
|
|
825
|
+
}
|
|
826
|
+
get remark() {
|
|
827
|
+
return this._remark;
|
|
828
|
+
}
|
|
829
|
+
get trackApiCode() {
|
|
830
|
+
return this._trackApiCode;
|
|
831
|
+
}
|
|
832
|
+
get items() {
|
|
833
|
+
return this._items;
|
|
834
|
+
}
|
|
835
|
+
get taxType() {
|
|
836
|
+
return this._taxType;
|
|
837
|
+
}
|
|
838
|
+
get customsClearanceMark() {
|
|
839
|
+
return this._customsClearanceMark;
|
|
840
|
+
}
|
|
841
|
+
get zeroTaxRateReason() {
|
|
842
|
+
return this._zeroTaxRateReason;
|
|
843
|
+
}
|
|
844
|
+
get brandName() {
|
|
845
|
+
return this._brandName;
|
|
846
|
+
}
|
|
847
|
+
get pricesIncludeTax() {
|
|
848
|
+
return this._pricesIncludeTax;
|
|
849
|
+
}
|
|
850
|
+
get status() {
|
|
851
|
+
return this._status;
|
|
852
|
+
}
|
|
853
|
+
get type() {
|
|
854
|
+
return this._type;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Check if this is a B2B invoice (has buyer tax ID)
|
|
858
|
+
*/
|
|
859
|
+
get isB2B() {
|
|
860
|
+
return this._buyer.isCompany;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Check if invoice has been issued
|
|
864
|
+
*/
|
|
865
|
+
get isIssued() {
|
|
866
|
+
return this._invoiceNumber !== void 0;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Check if invoice has carrier
|
|
870
|
+
*/
|
|
871
|
+
get hasCarrier() {
|
|
872
|
+
return !this._carrier.isEmpty;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Check if invoice is donated
|
|
876
|
+
*/
|
|
877
|
+
get isDonated() {
|
|
878
|
+
return this._donation.isDonating;
|
|
879
|
+
}
|
|
880
|
+
// --- Backward compatibility getters (deprecated) ---
|
|
881
|
+
/** @deprecated Use buyer.taxId instead */
|
|
882
|
+
get buyerTaxId() {
|
|
883
|
+
return this._buyer.taxId;
|
|
884
|
+
}
|
|
885
|
+
/** @deprecated Use buyer.name instead */
|
|
886
|
+
get buyerName() {
|
|
887
|
+
return this._buyer.name;
|
|
888
|
+
}
|
|
889
|
+
/** @deprecated Use buyer.address instead */
|
|
890
|
+
get buyerAddress() {
|
|
891
|
+
return this._buyer.address;
|
|
892
|
+
}
|
|
893
|
+
/** @deprecated Use buyer.phone instead */
|
|
894
|
+
get buyerPhone() {
|
|
895
|
+
return this._buyer.phone;
|
|
896
|
+
}
|
|
897
|
+
/** @deprecated Use buyer.email instead */
|
|
898
|
+
get buyerEmail() {
|
|
899
|
+
return this._buyer.email;
|
|
900
|
+
}
|
|
901
|
+
/** @deprecated Use donation.code instead */
|
|
902
|
+
get donationCode() {
|
|
903
|
+
return this._donation.code;
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
// src/domain/allowance/Allowance.ts
|
|
908
|
+
var AllowanceStatus = /* @__PURE__ */ ((AllowanceStatus2) => {
|
|
909
|
+
AllowanceStatus2[AllowanceStatus2["PENDING"] = 1] = "PENDING";
|
|
910
|
+
AllowanceStatus2[AllowanceStatus2["UPLOADING"] = 2] = "UPLOADING";
|
|
911
|
+
AllowanceStatus2[AllowanceStatus2["UPLOADED"] = 3] = "UPLOADED";
|
|
912
|
+
AllowanceStatus2[AllowanceStatus2["PROCESSING"] = 31] = "PROCESSING";
|
|
913
|
+
AllowanceStatus2[AllowanceStatus2["AWAITING_CONFIRMATION"] = 32] = "AWAITING_CONFIRMATION";
|
|
914
|
+
AllowanceStatus2[AllowanceStatus2["ERROR"] = 91] = "ERROR";
|
|
915
|
+
AllowanceStatus2[AllowanceStatus2["COMPLETED"] = 99] = "COMPLETED";
|
|
916
|
+
return AllowanceStatus2;
|
|
917
|
+
})(AllowanceStatus || {});
|
|
918
|
+
var AllowanceType = /* @__PURE__ */ ((AllowanceType2) => {
|
|
919
|
+
AllowanceType2["B2C_ISSUE"] = "C0701";
|
|
920
|
+
AllowanceType2["B2C_VOID"] = "C0801";
|
|
921
|
+
AllowanceType2["B2B_ISSUE"] = "A0701";
|
|
922
|
+
AllowanceType2["B2B_VOID"] = "A0801";
|
|
923
|
+
return AllowanceType2;
|
|
924
|
+
})(AllowanceType || {});
|
|
925
|
+
var Allowance = class _Allowance {
|
|
926
|
+
constructor(_originalInvoiceNumber, _originalInvoiceDate, _buyerTaxId, _sellerTaxId, _buyerName, _sellerName, _items, _taxType, _pricesIncludeTax = true, _status = 1 /* PENDING */, _type = "C0701" /* B2C_ISSUE */) {
|
|
927
|
+
this._originalInvoiceNumber = _originalInvoiceNumber;
|
|
928
|
+
this._originalInvoiceDate = _originalInvoiceDate;
|
|
929
|
+
this._buyerTaxId = _buyerTaxId;
|
|
930
|
+
this._sellerTaxId = _sellerTaxId;
|
|
931
|
+
this._buyerName = _buyerName;
|
|
932
|
+
this._sellerName = _sellerName;
|
|
933
|
+
this._items = _items;
|
|
934
|
+
this._taxType = _taxType;
|
|
935
|
+
this._pricesIncludeTax = _pricesIncludeTax;
|
|
936
|
+
this._status = _status;
|
|
937
|
+
this._type = _type;
|
|
938
|
+
}
|
|
939
|
+
_allowanceNumber;
|
|
940
|
+
_allowanceDate;
|
|
941
|
+
/**
|
|
942
|
+
* Create a new Allowance
|
|
943
|
+
*/
|
|
944
|
+
static create(props) {
|
|
945
|
+
if (!props.items || props.items.length === 0) {
|
|
946
|
+
throw new ValidationError("items", "At least one item is required");
|
|
947
|
+
}
|
|
948
|
+
if (props.items.length > 9999) {
|
|
949
|
+
throw new ValidationError("items", "Maximum 9999 items allowed");
|
|
950
|
+
}
|
|
951
|
+
const taxType = _Allowance.determineTaxType(props.items);
|
|
952
|
+
const isB2B = !props.buyerTaxId.isNone();
|
|
953
|
+
return new _Allowance(
|
|
954
|
+
props.originalInvoiceNumber,
|
|
955
|
+
props.originalInvoiceDate,
|
|
956
|
+
props.buyerTaxId,
|
|
957
|
+
props.sellerTaxId,
|
|
958
|
+
props.buyerName?.trim() ?? "",
|
|
959
|
+
props.sellerName?.trim() ?? "",
|
|
960
|
+
props.items,
|
|
961
|
+
taxType,
|
|
962
|
+
props.pricesIncludeTax ?? true,
|
|
963
|
+
1 /* PENDING */,
|
|
964
|
+
isB2B ? "A0701" /* B2B_ISSUE */ : "C0701" /* B2C_ISSUE */
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Determine overall tax type from items
|
|
969
|
+
*/
|
|
970
|
+
static determineTaxType(items) {
|
|
971
|
+
const taxTypes = new Set(items.map((item) => item.taxType));
|
|
972
|
+
if (taxTypes.size === 1) {
|
|
973
|
+
return items[0].taxType;
|
|
974
|
+
}
|
|
975
|
+
return 9 /* MIXED */;
|
|
976
|
+
}
|
|
977
|
+
// --- Calculated amounts ---
|
|
978
|
+
/**
|
|
979
|
+
* Calculate taxable sales amount (應稅銷售額)
|
|
980
|
+
*/
|
|
981
|
+
calculateTaxableAmount() {
|
|
982
|
+
return this._items.filter((item) => item.taxType === 1 /* TAXABLE */).reduce((sum, item) => sum.add(item.amount), Money.zero()).round();
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Calculate free tax sales amount (免稅銷售額)
|
|
986
|
+
*/
|
|
987
|
+
calculateFreeTaxAmount() {
|
|
988
|
+
return this._items.filter((item) => item.taxType === 3 /* TAX_EXEMPT */).reduce((sum, item) => sum.add(item.amount), Money.zero()).round();
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Calculate zero tax sales amount (零稅率銷售額)
|
|
992
|
+
*/
|
|
993
|
+
calculateZeroTaxAmount() {
|
|
994
|
+
return this._items.filter((item) => item.taxType === 2 /* ZERO_RATED */).reduce((sum, item) => sum.add(item.amount), Money.zero()).round();
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Calculate tax amount (營業稅額)
|
|
998
|
+
*/
|
|
999
|
+
calculateTaxAmount() {
|
|
1000
|
+
const taxableAmount = this.calculateTaxableAmount();
|
|
1001
|
+
if (this._pricesIncludeTax) {
|
|
1002
|
+
const beforeTax = taxableAmount.divide(1 + TAX_RATE).round();
|
|
1003
|
+
return taxableAmount.subtract(beforeTax);
|
|
1004
|
+
} else {
|
|
1005
|
+
return taxableAmount.multiply(TAX_RATE).round();
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Calculate total amount (總計)
|
|
1010
|
+
*/
|
|
1011
|
+
calculateTotalAmount() {
|
|
1012
|
+
const taxableAmount = this.calculateTaxableAmount();
|
|
1013
|
+
const freeTaxAmount = this.calculateFreeTaxAmount();
|
|
1014
|
+
const zeroTaxAmount = this.calculateZeroTaxAmount();
|
|
1015
|
+
const taxAmount = this.calculateTaxAmount();
|
|
1016
|
+
if (this._pricesIncludeTax) {
|
|
1017
|
+
return taxableAmount.add(freeTaxAmount).add(zeroTaxAmount);
|
|
1018
|
+
} else {
|
|
1019
|
+
return taxableAmount.add(freeTaxAmount).add(zeroTaxAmount).add(taxAmount);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
// --- State mutations ---
|
|
1023
|
+
/**
|
|
1024
|
+
* Set allowance number after issuing
|
|
1025
|
+
*/
|
|
1026
|
+
setAllowanceNumber(number, date) {
|
|
1027
|
+
this._allowanceNumber = number;
|
|
1028
|
+
this._allowanceDate = date;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Update status
|
|
1032
|
+
*/
|
|
1033
|
+
updateStatus(status) {
|
|
1034
|
+
this._status = status;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Mark as voided
|
|
1038
|
+
*/
|
|
1039
|
+
markAsVoided() {
|
|
1040
|
+
this._type = this.isB2B ? "A0801" /* B2B_VOID */ : "C0801" /* B2C_VOID */;
|
|
1041
|
+
}
|
|
1042
|
+
// --- Getters ---
|
|
1043
|
+
get originalInvoiceNumber() {
|
|
1044
|
+
return this._originalInvoiceNumber;
|
|
1045
|
+
}
|
|
1046
|
+
get originalInvoiceDate() {
|
|
1047
|
+
return this._originalInvoiceDate;
|
|
1048
|
+
}
|
|
1049
|
+
get allowanceNumber() {
|
|
1050
|
+
return this._allowanceNumber;
|
|
1051
|
+
}
|
|
1052
|
+
get allowanceDate() {
|
|
1053
|
+
return this._allowanceDate;
|
|
1054
|
+
}
|
|
1055
|
+
get buyerTaxId() {
|
|
1056
|
+
return this._buyerTaxId;
|
|
1057
|
+
}
|
|
1058
|
+
get sellerTaxId() {
|
|
1059
|
+
return this._sellerTaxId;
|
|
1060
|
+
}
|
|
1061
|
+
get buyerName() {
|
|
1062
|
+
return this._buyerName;
|
|
1063
|
+
}
|
|
1064
|
+
get sellerName() {
|
|
1065
|
+
return this._sellerName;
|
|
1066
|
+
}
|
|
1067
|
+
get items() {
|
|
1068
|
+
return this._items;
|
|
1069
|
+
}
|
|
1070
|
+
get taxType() {
|
|
1071
|
+
return this._taxType;
|
|
1072
|
+
}
|
|
1073
|
+
get pricesIncludeTax() {
|
|
1074
|
+
return this._pricesIncludeTax;
|
|
1075
|
+
}
|
|
1076
|
+
get status() {
|
|
1077
|
+
return this._status;
|
|
1078
|
+
}
|
|
1079
|
+
get type() {
|
|
1080
|
+
return this._type;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Check if this is a B2B allowance (has buyer tax ID)
|
|
1084
|
+
*/
|
|
1085
|
+
get isB2B() {
|
|
1086
|
+
return !this._buyerTaxId.isNone();
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Check if allowance has been issued
|
|
1090
|
+
*/
|
|
1091
|
+
get isIssued() {
|
|
1092
|
+
return this._allowanceNumber !== void 0;
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
// src/infrastructure/amego/AmegoMapper.ts
|
|
1097
|
+
var AmegoMapper = class _AmegoMapper {
|
|
1098
|
+
/**
|
|
1099
|
+
* Convert Invoice domain object to API payload
|
|
1100
|
+
*/
|
|
1101
|
+
static toInvoicePayload(invoice) {
|
|
1102
|
+
const salesAmount = invoice.calculateSalesAmount().toNumber();
|
|
1103
|
+
const freeTaxSalesAmount = invoice.calculateFreeTaxSalesAmount().toNumber();
|
|
1104
|
+
const zeroTaxSalesAmount = invoice.calculateZeroTaxSalesAmount().toNumber();
|
|
1105
|
+
const taxAmount = invoice.calculateTaxAmount().toNumber();
|
|
1106
|
+
const totalAmount = invoice.calculateTotalAmount().toNumber();
|
|
1107
|
+
const payload = {
|
|
1108
|
+
OrderId: invoice.orderId.toString(),
|
|
1109
|
+
BuyerIdentifier: invoice.buyer.taxId.isNone() ? "0000000000" : invoice.buyer.taxId.toString(),
|
|
1110
|
+
BuyerName: invoice.buyer.name,
|
|
1111
|
+
ProductItem: invoice.items.map(_AmegoMapper.toInvoiceItemPayload),
|
|
1112
|
+
SalesAmount: salesAmount,
|
|
1113
|
+
FreeTaxSalesAmount: freeTaxSalesAmount,
|
|
1114
|
+
ZeroTaxSalesAmount: zeroTaxSalesAmount,
|
|
1115
|
+
TaxType: _AmegoMapper.toApiTaxType(invoice.taxType),
|
|
1116
|
+
TaxRate: "0.05",
|
|
1117
|
+
TaxAmount: taxAmount,
|
|
1118
|
+
TotalAmount: totalAmount,
|
|
1119
|
+
DetailVat: invoice.pricesIncludeTax ? 1 : 0
|
|
1120
|
+
};
|
|
1121
|
+
if (invoice.trackApiCode) {
|
|
1122
|
+
payload.TrackApiCode = invoice.trackApiCode;
|
|
1123
|
+
}
|
|
1124
|
+
if (invoice.buyer.address) {
|
|
1125
|
+
payload.BuyerAddress = invoice.buyer.address;
|
|
1126
|
+
}
|
|
1127
|
+
if (invoice.buyer.phone) {
|
|
1128
|
+
payload.BuyerTelephoneNumber = invoice.buyer.phone;
|
|
1129
|
+
}
|
|
1130
|
+
if (invoice.buyer.email) {
|
|
1131
|
+
payload.BuyerEmailAddress = invoice.buyer.email;
|
|
1132
|
+
}
|
|
1133
|
+
if (invoice.remark) {
|
|
1134
|
+
payload.MainRemark = invoice.remark;
|
|
1135
|
+
}
|
|
1136
|
+
if (invoice.hasCarrier) {
|
|
1137
|
+
payload.CarrierType = invoice.carrier.typeCode;
|
|
1138
|
+
payload.CarrierId1 = invoice.carrier.id;
|
|
1139
|
+
payload.CarrierId2 = invoice.carrier.id;
|
|
1140
|
+
}
|
|
1141
|
+
if (invoice.isDonated) {
|
|
1142
|
+
payload.NPOBAN = invoice.donation.code;
|
|
1143
|
+
}
|
|
1144
|
+
if (invoice.customsClearanceMark !== void 0) {
|
|
1145
|
+
payload.CustomsClearanceMark = invoice.customsClearanceMark;
|
|
1146
|
+
}
|
|
1147
|
+
if (invoice.zeroTaxRateReason !== void 0) {
|
|
1148
|
+
payload.ZeroTaxRateReason = invoice.zeroTaxRateReason;
|
|
1149
|
+
}
|
|
1150
|
+
if (invoice.brandName) {
|
|
1151
|
+
payload.BrandName = invoice.brandName;
|
|
1152
|
+
}
|
|
1153
|
+
return payload;
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Convert InvoiceItem to API payload (ProductItem)
|
|
1157
|
+
*/
|
|
1158
|
+
static toInvoiceItemPayload(item) {
|
|
1159
|
+
const payload = {
|
|
1160
|
+
Description: item.description,
|
|
1161
|
+
Quantity: item.quantity,
|
|
1162
|
+
UnitPrice: item.unitPrice.toNumber(),
|
|
1163
|
+
Amount: item.amount.toNumber(),
|
|
1164
|
+
TaxType: _AmegoMapper.toApiTaxType(item.taxType)
|
|
1165
|
+
};
|
|
1166
|
+
if (item.unit) {
|
|
1167
|
+
payload.Unit = item.unit;
|
|
1168
|
+
}
|
|
1169
|
+
if (item.remark) {
|
|
1170
|
+
payload.Remark = item.remark;
|
|
1171
|
+
}
|
|
1172
|
+
return payload;
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Convert Allowance domain object to API payload
|
|
1176
|
+
*/
|
|
1177
|
+
static toAllowancePayload(allowance, allowanceNumber) {
|
|
1178
|
+
let totalAmount = 0;
|
|
1179
|
+
let taxAmount = 0;
|
|
1180
|
+
const items = allowance.items.map((item) => {
|
|
1181
|
+
const amount = item.amount.toNumber();
|
|
1182
|
+
const tax = Math.round(amount * 0.05);
|
|
1183
|
+
totalAmount += amount;
|
|
1184
|
+
taxAmount += tax;
|
|
1185
|
+
return {
|
|
1186
|
+
OriginalInvoiceNumber: allowance.originalInvoiceNumber.toString(),
|
|
1187
|
+
OriginalInvoiceDate: _AmegoMapper.formatDateNumber(
|
|
1188
|
+
allowance.originalInvoiceDate
|
|
1189
|
+
),
|
|
1190
|
+
OriginalDescription: item.originalDescription,
|
|
1191
|
+
Quantity: item.quantity,
|
|
1192
|
+
UnitPrice: item.unitPrice.toNumber(),
|
|
1193
|
+
Amount: amount,
|
|
1194
|
+
Tax: tax,
|
|
1195
|
+
TaxType: _AmegoMapper.toApiTaxType(item.taxType),
|
|
1196
|
+
Unit: item.unit
|
|
1197
|
+
};
|
|
1198
|
+
});
|
|
1199
|
+
const payload = {
|
|
1200
|
+
AllowanceNumber: allowanceNumber,
|
|
1201
|
+
AllowanceDate: _AmegoMapper.formatDate(/* @__PURE__ */ new Date()),
|
|
1202
|
+
AllowanceType: 2,
|
|
1203
|
+
// 賣方折讓證明通知單
|
|
1204
|
+
BuyerIdentifier: allowance.buyerTaxId.isNone() ? "0000000000" : allowance.buyerTaxId.toString(),
|
|
1205
|
+
BuyerName: allowance.buyerName ?? "",
|
|
1206
|
+
ProductItem: items,
|
|
1207
|
+
TaxAmount: taxAmount,
|
|
1208
|
+
TotalAmount: totalAmount
|
|
1209
|
+
};
|
|
1210
|
+
return payload;
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Convert AllowanceItem to API payload
|
|
1214
|
+
*/
|
|
1215
|
+
static toAllowanceItemPayload(item, originalInvoiceNumber, originalInvoiceDate) {
|
|
1216
|
+
const amount = item.amount.toNumber();
|
|
1217
|
+
const tax = Math.round(amount * 0.05);
|
|
1218
|
+
return {
|
|
1219
|
+
OriginalInvoiceNumber: originalInvoiceNumber,
|
|
1220
|
+
OriginalInvoiceDate: _AmegoMapper.formatDateNumber(originalInvoiceDate),
|
|
1221
|
+
OriginalDescription: item.originalDescription,
|
|
1222
|
+
Quantity: item.quantity,
|
|
1223
|
+
UnitPrice: item.unitPrice.toNumber(),
|
|
1224
|
+
Amount: amount,
|
|
1225
|
+
Tax: tax,
|
|
1226
|
+
TaxType: _AmegoMapper.toApiTaxType(item.taxType),
|
|
1227
|
+
Unit: item.unit
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Convert domain TaxType to API tax type number
|
|
1232
|
+
*/
|
|
1233
|
+
static toApiTaxType(taxType) {
|
|
1234
|
+
switch (taxType) {
|
|
1235
|
+
case 1 /* TAXABLE */:
|
|
1236
|
+
return 1;
|
|
1237
|
+
case 2 /* ZERO_RATED */:
|
|
1238
|
+
return 2;
|
|
1239
|
+
case 3 /* TAX_EXEMPT */:
|
|
1240
|
+
return 3;
|
|
1241
|
+
case 9 /* MIXED */:
|
|
1242
|
+
return 9;
|
|
1243
|
+
default:
|
|
1244
|
+
return 1;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Convert API tax type number to domain TaxType
|
|
1249
|
+
*/
|
|
1250
|
+
static fromApiTaxType(apiTaxType) {
|
|
1251
|
+
switch (apiTaxType) {
|
|
1252
|
+
case 1:
|
|
1253
|
+
return 1 /* TAXABLE */;
|
|
1254
|
+
case 2:
|
|
1255
|
+
return 2 /* ZERO_RATED */;
|
|
1256
|
+
case 3:
|
|
1257
|
+
return 3 /* TAX_EXEMPT */;
|
|
1258
|
+
case 4:
|
|
1259
|
+
return 1 /* TAXABLE */;
|
|
1260
|
+
// 特種稅率 -> 應稅
|
|
1261
|
+
case 9:
|
|
1262
|
+
return 9 /* MIXED */;
|
|
1263
|
+
default:
|
|
1264
|
+
return 1 /* TAXABLE */;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Convert API status code to InvoiceStatus
|
|
1269
|
+
*/
|
|
1270
|
+
static toInvoiceStatus(status) {
|
|
1271
|
+
switch (status) {
|
|
1272
|
+
case 1:
|
|
1273
|
+
return 1 /* PENDING */;
|
|
1274
|
+
case 2:
|
|
1275
|
+
return 2 /* UPLOADING */;
|
|
1276
|
+
case 3:
|
|
1277
|
+
return 3 /* UPLOADED */;
|
|
1278
|
+
case 31:
|
|
1279
|
+
return 31 /* PROCESSING */;
|
|
1280
|
+
case 32:
|
|
1281
|
+
return 32 /* AWAITING_CONFIRMATION */;
|
|
1282
|
+
case 91:
|
|
1283
|
+
return 91 /* ERROR */;
|
|
1284
|
+
case 99:
|
|
1285
|
+
return 99 /* COMPLETED */;
|
|
1286
|
+
default:
|
|
1287
|
+
return 1 /* PENDING */;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Convert API status code to AllowanceStatus
|
|
1292
|
+
*/
|
|
1293
|
+
static toAllowanceStatus(status) {
|
|
1294
|
+
switch (status) {
|
|
1295
|
+
case 1:
|
|
1296
|
+
return 1 /* PENDING */;
|
|
1297
|
+
case 2:
|
|
1298
|
+
return 2 /* UPLOADING */;
|
|
1299
|
+
case 3:
|
|
1300
|
+
return 3 /* UPLOADED */;
|
|
1301
|
+
case 31:
|
|
1302
|
+
return 31 /* PROCESSING */;
|
|
1303
|
+
case 32:
|
|
1304
|
+
return 32 /* AWAITING_CONFIRMATION */;
|
|
1305
|
+
case 91:
|
|
1306
|
+
return 91 /* ERROR */;
|
|
1307
|
+
case 99:
|
|
1308
|
+
return 99 /* COMPLETED */;
|
|
1309
|
+
default:
|
|
1310
|
+
return 1 /* PENDING */;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Format date to YYYYMMDD string
|
|
1315
|
+
*/
|
|
1316
|
+
static formatDate(date) {
|
|
1317
|
+
const year = date.getFullYear();
|
|
1318
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1319
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1320
|
+
return `${year}${month}${day}`;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Format date to YYYYMMDD number
|
|
1324
|
+
*/
|
|
1325
|
+
static formatDateNumber(date) {
|
|
1326
|
+
return parseInt(_AmegoMapper.formatDate(date), 10);
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Parse YYYYMMDD (string or number) to Date
|
|
1330
|
+
*/
|
|
1331
|
+
static parseDate(dateValue) {
|
|
1332
|
+
const dateStr = String(dateValue);
|
|
1333
|
+
const year = parseInt(dateStr.substring(0, 4), 10);
|
|
1334
|
+
const month = parseInt(dateStr.substring(4, 6), 10) - 1;
|
|
1335
|
+
const day = parseInt(dateStr.substring(6, 8), 10);
|
|
1336
|
+
return new Date(year, month, day);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Parse Unix timestamp to Date
|
|
1340
|
+
*/
|
|
1341
|
+
static parseUnixTimestamp(timestamp) {
|
|
1342
|
+
return new Date(timestamp * 1e3);
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Parse ISO date string to Date
|
|
1346
|
+
*/
|
|
1347
|
+
static parseIsoDate(isoStr) {
|
|
1348
|
+
return new Date(isoStr);
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
// src/domain/invoice/InvoiceNumber.ts
|
|
1353
|
+
var InvoiceNumber = class _InvoiceNumber {
|
|
1354
|
+
constructor(value) {
|
|
1355
|
+
this.value = value;
|
|
1356
|
+
}
|
|
1357
|
+
static PATTERN = /^[A-Z]{2}\d{8}$/;
|
|
1358
|
+
/**
|
|
1359
|
+
* Create an InvoiceNumber from string
|
|
1360
|
+
* @throws InvalidInvoiceNumberError if format is invalid
|
|
1361
|
+
*/
|
|
1362
|
+
static create(value) {
|
|
1363
|
+
const normalized = value.trim().toUpperCase();
|
|
1364
|
+
if (!_InvoiceNumber.isValid(normalized)) {
|
|
1365
|
+
throw new InvalidInvoiceNumberError(value);
|
|
1366
|
+
}
|
|
1367
|
+
return new _InvoiceNumber(normalized);
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Try to create an InvoiceNumber, return null if invalid
|
|
1371
|
+
*/
|
|
1372
|
+
static tryCreate(value) {
|
|
1373
|
+
try {
|
|
1374
|
+
return _InvoiceNumber.create(value);
|
|
1375
|
+
} catch {
|
|
1376
|
+
return null;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Validate invoice number format
|
|
1381
|
+
*/
|
|
1382
|
+
static isValid(value) {
|
|
1383
|
+
return _InvoiceNumber.PATTERN.test(value);
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Get the track (字軌) - first 2 letters
|
|
1387
|
+
*/
|
|
1388
|
+
getTrack() {
|
|
1389
|
+
return this.value.substring(0, 2);
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Get the number part - last 8 digits
|
|
1393
|
+
*/
|
|
1394
|
+
getNumber() {
|
|
1395
|
+
return this.value.substring(2);
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Get the full invoice number
|
|
1399
|
+
*/
|
|
1400
|
+
toString() {
|
|
1401
|
+
return this.value;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Check equality
|
|
1405
|
+
*/
|
|
1406
|
+
equals(other) {
|
|
1407
|
+
return this.value === other.value;
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
// src/domain/invoice/InvoiceItem.ts
|
|
1412
|
+
var InvoiceItem = class _InvoiceItem {
|
|
1413
|
+
constructor(_description, _quantity, _unitPrice, _amount, _unit, _remark, _taxType) {
|
|
1414
|
+
this._description = _description;
|
|
1415
|
+
this._quantity = _quantity;
|
|
1416
|
+
this._unitPrice = _unitPrice;
|
|
1417
|
+
this._amount = _amount;
|
|
1418
|
+
this._unit = _unit;
|
|
1419
|
+
this._remark = _remark;
|
|
1420
|
+
this._taxType = _taxType;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Create an InvoiceItem
|
|
1424
|
+
*/
|
|
1425
|
+
static create(props) {
|
|
1426
|
+
if (!props.description || props.description.trim().length === 0) {
|
|
1427
|
+
throw new ValidationError("description", "Description is required");
|
|
1428
|
+
}
|
|
1429
|
+
if (props.description.length > 256) {
|
|
1430
|
+
throw new ValidationError(
|
|
1431
|
+
"description",
|
|
1432
|
+
"Description must not exceed 256 characters"
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
if (props.quantity === 0) {
|
|
1436
|
+
throw new ValidationError("quantity", "Quantity cannot be zero");
|
|
1437
|
+
}
|
|
1438
|
+
if (props.unit && props.unit.length > 6) {
|
|
1439
|
+
throw new ValidationError("unit", "Unit must not exceed 6 characters");
|
|
1440
|
+
}
|
|
1441
|
+
if (props.remark && props.remark.length > 40) {
|
|
1442
|
+
throw new ValidationError("remark", "Remark must not exceed 40 characters");
|
|
1443
|
+
}
|
|
1444
|
+
return new _InvoiceItem(
|
|
1445
|
+
props.description.trim(),
|
|
1446
|
+
props.quantity,
|
|
1447
|
+
props.unitPrice,
|
|
1448
|
+
props.amount,
|
|
1449
|
+
props.unit?.trim() ?? "",
|
|
1450
|
+
props.remark?.trim() ?? "",
|
|
1451
|
+
props.taxType ?? 1 /* TAXABLE */
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Create an InvoiceItem with auto-calculated amount
|
|
1456
|
+
*/
|
|
1457
|
+
static createWithAutoAmount(props) {
|
|
1458
|
+
const amount = props.unitPrice.multiply(props.quantity);
|
|
1459
|
+
return _InvoiceItem.create({ ...props, amount });
|
|
1460
|
+
}
|
|
1461
|
+
// Getters
|
|
1462
|
+
get description() {
|
|
1463
|
+
return this._description;
|
|
1464
|
+
}
|
|
1465
|
+
get quantity() {
|
|
1466
|
+
return this._quantity;
|
|
1467
|
+
}
|
|
1468
|
+
get unitPrice() {
|
|
1469
|
+
return this._unitPrice;
|
|
1470
|
+
}
|
|
1471
|
+
get amount() {
|
|
1472
|
+
return this._amount;
|
|
1473
|
+
}
|
|
1474
|
+
get unit() {
|
|
1475
|
+
return this._unit;
|
|
1476
|
+
}
|
|
1477
|
+
get remark() {
|
|
1478
|
+
return this._remark;
|
|
1479
|
+
}
|
|
1480
|
+
get taxType() {
|
|
1481
|
+
return this._taxType;
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Check if this is a discount item (negative amount)
|
|
1485
|
+
*/
|
|
1486
|
+
isDiscount() {
|
|
1487
|
+
return this._amount.isNegative();
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
// src/domain/shared/TaxId.ts
|
|
1492
|
+
var TaxId = class _TaxId {
|
|
1493
|
+
constructor(value) {
|
|
1494
|
+
this.value = value;
|
|
1495
|
+
}
|
|
1496
|
+
static WEIGHTS = [1, 2, 1, 2, 1, 2, 4, 1];
|
|
1497
|
+
static NO_TAX_ID = "0000000000";
|
|
1498
|
+
/**
|
|
1499
|
+
* Create a TaxId from string
|
|
1500
|
+
* @throws InvalidTaxIdError if the tax ID is invalid
|
|
1501
|
+
*/
|
|
1502
|
+
static create(value) {
|
|
1503
|
+
const normalized = value.trim();
|
|
1504
|
+
if (!_TaxId.isValid(normalized)) {
|
|
1505
|
+
throw new InvalidTaxIdError(value);
|
|
1506
|
+
}
|
|
1507
|
+
return new _TaxId(normalized);
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Create a "no tax ID" placeholder (0000000000)
|
|
1511
|
+
* Used for B2C invoices without buyer's tax ID
|
|
1512
|
+
*/
|
|
1513
|
+
static none() {
|
|
1514
|
+
return new _TaxId(_TaxId.NO_TAX_ID);
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Try to create a TaxId, return null if invalid
|
|
1518
|
+
*/
|
|
1519
|
+
static tryCreate(value) {
|
|
1520
|
+
try {
|
|
1521
|
+
return _TaxId.create(value);
|
|
1522
|
+
} catch {
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Validate a tax ID string
|
|
1528
|
+
*/
|
|
1529
|
+
static isValid(value) {
|
|
1530
|
+
if (value === _TaxId.NO_TAX_ID) {
|
|
1531
|
+
return true;
|
|
1532
|
+
}
|
|
1533
|
+
if (!/^\d{8}$/.test(value)) {
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
return _TaxId.validateChecksum(value);
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Validate checksum using the official algorithm
|
|
1540
|
+
*
|
|
1541
|
+
* Algorithm:
|
|
1542
|
+
* 1. Multiply each digit by its weight [1,2,1,2,1,2,4,1]
|
|
1543
|
+
* 2. For each product, sum its digits (e.g., 18 -> 1+8=9)
|
|
1544
|
+
* 3. Sum all results
|
|
1545
|
+
* 4. If 7th digit is 7, check both (sum % 10 === 0) or ((sum + 1) % 10 === 0)
|
|
1546
|
+
* 5. Otherwise, check (sum % 10 === 0)
|
|
1547
|
+
*/
|
|
1548
|
+
static validateChecksum(value) {
|
|
1549
|
+
const digits = value.split("").map(Number);
|
|
1550
|
+
let sum = 0;
|
|
1551
|
+
for (let i = 0; i < 8; i++) {
|
|
1552
|
+
const product = digits[i] * _TaxId.WEIGHTS[i];
|
|
1553
|
+
sum += Math.floor(product / 10) + product % 10;
|
|
1554
|
+
}
|
|
1555
|
+
if (digits[6] === 7) {
|
|
1556
|
+
return sum % 10 === 0 || (sum + 1) % 10 === 0;
|
|
1557
|
+
}
|
|
1558
|
+
return sum % 10 === 0;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Check if this is a "no tax ID" placeholder
|
|
1562
|
+
*/
|
|
1563
|
+
isNone() {
|
|
1564
|
+
return this.value === _TaxId.NO_TAX_ID;
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Get the string value
|
|
1568
|
+
*/
|
|
1569
|
+
toString() {
|
|
1570
|
+
return this.value;
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Check equality with another TaxId
|
|
1574
|
+
*/
|
|
1575
|
+
equals(other) {
|
|
1576
|
+
return this.value === other.value;
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
// src/domain/shared/OrderId.ts
|
|
1581
|
+
var OrderId = class _OrderId {
|
|
1582
|
+
constructor(value) {
|
|
1583
|
+
this.value = value;
|
|
1584
|
+
}
|
|
1585
|
+
static MAX_LENGTH = 40;
|
|
1586
|
+
/**
|
|
1587
|
+
* Create an OrderId
|
|
1588
|
+
* @throws ValidationError if invalid
|
|
1589
|
+
*/
|
|
1590
|
+
static create(value) {
|
|
1591
|
+
const trimmed = value?.trim();
|
|
1592
|
+
if (!trimmed || trimmed.length === 0) {
|
|
1593
|
+
throw new ValidationError("orderId", "Order ID is required");
|
|
1594
|
+
}
|
|
1595
|
+
if (trimmed.length > _OrderId.MAX_LENGTH) {
|
|
1596
|
+
throw new ValidationError(
|
|
1597
|
+
"orderId",
|
|
1598
|
+
`Order ID must not exceed ${_OrderId.MAX_LENGTH} characters`
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
return new _OrderId(trimmed);
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Try to create an OrderId, returns null if invalid
|
|
1605
|
+
*/
|
|
1606
|
+
static tryCreate(value) {
|
|
1607
|
+
try {
|
|
1608
|
+
return _OrderId.create(value);
|
|
1609
|
+
} catch {
|
|
1610
|
+
return null;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Get the string value
|
|
1615
|
+
*/
|
|
1616
|
+
toString() {
|
|
1617
|
+
return this.value;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Check equality
|
|
1621
|
+
*/
|
|
1622
|
+
equals(other) {
|
|
1623
|
+
return this.value === other.value;
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
// src/domain/shared/Buyer.ts
|
|
1628
|
+
var Buyer = class _Buyer {
|
|
1629
|
+
constructor(_type, _taxId, _name, _address, _phone, _email) {
|
|
1630
|
+
this._type = _type;
|
|
1631
|
+
this._taxId = _taxId;
|
|
1632
|
+
this._name = _name;
|
|
1633
|
+
this._address = _address;
|
|
1634
|
+
this._phone = _phone;
|
|
1635
|
+
this._email = _email;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Create an anonymous buyer (B2C)
|
|
1639
|
+
* @param name Buyer name (default: '消費者')
|
|
1640
|
+
*/
|
|
1641
|
+
static anonymous(name) {
|
|
1642
|
+
const trimmedName = name === void 0 ? "\u6D88\u8CBB\u8005" : name.trim();
|
|
1643
|
+
_Buyer.validateName(trimmedName);
|
|
1644
|
+
return new _Buyer(
|
|
1645
|
+
"anonymous" /* ANONYMOUS */,
|
|
1646
|
+
TaxId.none(),
|
|
1647
|
+
trimmedName,
|
|
1648
|
+
"",
|
|
1649
|
+
"",
|
|
1650
|
+
""
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Create a company buyer (B2B)
|
|
1655
|
+
*/
|
|
1656
|
+
static company(props) {
|
|
1657
|
+
const name = props.name?.trim();
|
|
1658
|
+
_Buyer.validateName(name);
|
|
1659
|
+
if (props.taxId.isNone()) {
|
|
1660
|
+
throw new ValidationError("taxId", "Company buyer must have a tax ID");
|
|
1661
|
+
}
|
|
1662
|
+
return new _Buyer(
|
|
1663
|
+
"company" /* COMPANY */,
|
|
1664
|
+
props.taxId,
|
|
1665
|
+
name,
|
|
1666
|
+
props.address?.trim() ?? "",
|
|
1667
|
+
props.phone?.trim() ?? "",
|
|
1668
|
+
props.email?.trim() ?? ""
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Validate buyer name
|
|
1673
|
+
*/
|
|
1674
|
+
static validateName(name) {
|
|
1675
|
+
if (!name || name.length === 0) {
|
|
1676
|
+
throw new ValidationError("buyerName", "Buyer name is required");
|
|
1677
|
+
}
|
|
1678
|
+
const invalidNames = ["0", "00", "000", "0000"];
|
|
1679
|
+
if (invalidNames.includes(name)) {
|
|
1680
|
+
throw new ValidationError(
|
|
1681
|
+
"buyerName",
|
|
1682
|
+
"Buyer name cannot be 0, 00, 000, or 0000"
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Get the buyer type
|
|
1688
|
+
*/
|
|
1689
|
+
get type() {
|
|
1690
|
+
return this._type;
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Get the tax ID
|
|
1694
|
+
*/
|
|
1695
|
+
get taxId() {
|
|
1696
|
+
return this._taxId;
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Get the name
|
|
1700
|
+
*/
|
|
1701
|
+
get name() {
|
|
1702
|
+
return this._name;
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Get the address
|
|
1706
|
+
*/
|
|
1707
|
+
get address() {
|
|
1708
|
+
return this._address;
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Get the phone
|
|
1712
|
+
*/
|
|
1713
|
+
get phone() {
|
|
1714
|
+
return this._phone;
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Get the email
|
|
1718
|
+
*/
|
|
1719
|
+
get email() {
|
|
1720
|
+
return this._email;
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Check if this is a B2B buyer (company)
|
|
1724
|
+
*/
|
|
1725
|
+
get isCompany() {
|
|
1726
|
+
return this._type === "company" /* COMPANY */;
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Check if this is a B2C buyer (anonymous)
|
|
1730
|
+
*/
|
|
1731
|
+
get isAnonymous() {
|
|
1732
|
+
return this._type === "anonymous" /* ANONYMOUS */;
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1736
|
+
// src/infrastructure/amego/AmegoInvoiceRepository.ts
|
|
1737
|
+
var AmegoInvoiceRepository = class {
|
|
1738
|
+
client;
|
|
1739
|
+
constructor(config) {
|
|
1740
|
+
this.client = new AmegoClient(config);
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Issue a new invoice (開立發票)
|
|
1744
|
+
*
|
|
1745
|
+
* 使用 /json/f0401 endpoint
|
|
1746
|
+
*/
|
|
1747
|
+
async issue(invoice) {
|
|
1748
|
+
const payload = AmegoMapper.toInvoicePayload(invoice);
|
|
1749
|
+
const response = await this.client.post(
|
|
1750
|
+
AMEGO_ENDPOINTS.INVOICE_ISSUE,
|
|
1751
|
+
payload
|
|
1752
|
+
);
|
|
1753
|
+
const invoiceNumber = InvoiceNumber.create(response.invoice_number);
|
|
1754
|
+
const invoiceTime = AmegoMapper.parseUnixTimestamp(response.invoice_time);
|
|
1755
|
+
invoice.setInvoiceNumber(
|
|
1756
|
+
invoiceNumber,
|
|
1757
|
+
invoiceTime,
|
|
1758
|
+
response.random_number
|
|
1759
|
+
);
|
|
1760
|
+
invoice.updateStatus(3 /* UPLOADED */);
|
|
1761
|
+
return {
|
|
1762
|
+
invoiceNumber,
|
|
1763
|
+
invoiceTime,
|
|
1764
|
+
randomNumber: response.random_number,
|
|
1765
|
+
barcode: response.barcode ?? "",
|
|
1766
|
+
qrcodeLeft: response.qrcode_left ?? "",
|
|
1767
|
+
qrcodeRight: response.qrcode_right ?? "",
|
|
1768
|
+
printData: response.base64_data
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Void an invoice (作廢發票)
|
|
1773
|
+
*
|
|
1774
|
+
* 使用 /json/f0501 endpoint
|
|
1775
|
+
*/
|
|
1776
|
+
async void(invoiceNumber) {
|
|
1777
|
+
const payload = {
|
|
1778
|
+
CancelInvoiceNumber: invoiceNumber.toString()
|
|
1779
|
+
};
|
|
1780
|
+
await this.client.post(AMEGO_ENDPOINTS.INVOICE_VOID, payload);
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Query invoice by invoice number (查詢發票 - 依發票號碼)
|
|
1784
|
+
*
|
|
1785
|
+
* 使用 /json/invoice_query endpoint
|
|
1786
|
+
*/
|
|
1787
|
+
async findByInvoiceNumber(invoiceNumber) {
|
|
1788
|
+
try {
|
|
1789
|
+
const payload = {
|
|
1790
|
+
type: "invoice",
|
|
1791
|
+
invoice_number: invoiceNumber.toString()
|
|
1792
|
+
};
|
|
1793
|
+
const response = await this.client.post(
|
|
1794
|
+
AMEGO_ENDPOINTS.INVOICE_QUERY,
|
|
1795
|
+
payload
|
|
1796
|
+
);
|
|
1797
|
+
if (!response.data) {
|
|
1798
|
+
return null;
|
|
1799
|
+
}
|
|
1800
|
+
return this.mapQueryResponse(response.data);
|
|
1801
|
+
} catch {
|
|
1802
|
+
return null;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Query invoice by order ID (查詢發票 - 依訂單編號)
|
|
1807
|
+
*
|
|
1808
|
+
* 使用 /json/invoice_query endpoint
|
|
1809
|
+
*/
|
|
1810
|
+
async findByOrderId(orderId) {
|
|
1811
|
+
try {
|
|
1812
|
+
const payload = {
|
|
1813
|
+
type: "order",
|
|
1814
|
+
order_id: orderId
|
|
1815
|
+
};
|
|
1816
|
+
const response = await this.client.post(
|
|
1817
|
+
AMEGO_ENDPOINTS.INVOICE_QUERY,
|
|
1818
|
+
payload
|
|
1819
|
+
);
|
|
1820
|
+
if (!response.data) {
|
|
1821
|
+
return null;
|
|
1822
|
+
}
|
|
1823
|
+
return this.mapQueryResponse(response.data);
|
|
1824
|
+
} catch {
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Check invoice status (查詢發票狀態)
|
|
1830
|
+
*
|
|
1831
|
+
* 使用 /json/invoice_status endpoint
|
|
1832
|
+
*/
|
|
1833
|
+
async getStatus(invoiceNumbers) {
|
|
1834
|
+
const results = [];
|
|
1835
|
+
for (const invoiceNumber of invoiceNumbers) {
|
|
1836
|
+
try {
|
|
1837
|
+
const payload = {
|
|
1838
|
+
InvoiceNumber: invoiceNumber.toString()
|
|
1839
|
+
};
|
|
1840
|
+
const response = await this.client.post(
|
|
1841
|
+
AMEGO_ENDPOINTS.INVOICE_STATUS,
|
|
1842
|
+
payload
|
|
1843
|
+
);
|
|
1844
|
+
if (response.data && response.data.length > 0) {
|
|
1845
|
+
for (const item of response.data) {
|
|
1846
|
+
results.push({
|
|
1847
|
+
invoiceNumber: item.invoice_number,
|
|
1848
|
+
type: item.type,
|
|
1849
|
+
status: AmegoMapper.toInvoiceStatus(item.status),
|
|
1850
|
+
totalAmount: item.total_amount
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
} catch {
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
return results;
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* List invoices (發票列表)
|
|
1861
|
+
*
|
|
1862
|
+
* 使用 /json/invoice_list endpoint
|
|
1863
|
+
*/
|
|
1864
|
+
async list(options) {
|
|
1865
|
+
const dateStart = parseInt(options.startDate, 10);
|
|
1866
|
+
const dateEnd = parseInt(options.endDate, 10);
|
|
1867
|
+
const payload = {
|
|
1868
|
+
date_select: options.dateType,
|
|
1869
|
+
date_start: dateStart,
|
|
1870
|
+
date_end: dateEnd,
|
|
1871
|
+
limit: options.limit ?? 20,
|
|
1872
|
+
page: options.page ?? 1
|
|
1873
|
+
};
|
|
1874
|
+
const response = await this.client.post(
|
|
1875
|
+
AMEGO_ENDPOINTS.INVOICE_LIST,
|
|
1876
|
+
payload
|
|
1877
|
+
);
|
|
1878
|
+
const invoices = (response.data ?? []).map(
|
|
1879
|
+
(item) => this.mapListItemResponse(item)
|
|
1880
|
+
);
|
|
1881
|
+
return {
|
|
1882
|
+
invoices,
|
|
1883
|
+
totalPages: response.page_total ?? 1,
|
|
1884
|
+
currentPage: response.page_now ?? 1,
|
|
1885
|
+
totalCount: response.data_total ?? 0
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Map API query response to domain query result
|
|
1890
|
+
*/
|
|
1891
|
+
mapQueryResponse(response) {
|
|
1892
|
+
const items = (response.product_item ?? []).map(
|
|
1893
|
+
(item) => InvoiceItem.create({
|
|
1894
|
+
description: item.description,
|
|
1895
|
+
quantity: item.quantity,
|
|
1896
|
+
unitPrice: Money.create(item.unit_price),
|
|
1897
|
+
amount: Money.create(item.amount),
|
|
1898
|
+
unit: item.unit || void 0,
|
|
1899
|
+
remark: item.remark || void 0,
|
|
1900
|
+
taxType: AmegoMapper.fromApiTaxType(item.tax_type)
|
|
1901
|
+
})
|
|
1902
|
+
);
|
|
1903
|
+
const buyerTaxId = TaxId.tryCreate(response.buyer_identifier) ?? TaxId.none();
|
|
1904
|
+
const buyer = buyerTaxId.isNone() ? Buyer.anonymous(response.buyer_name || "\u6D88\u8CBB\u8005") : Buyer.company({
|
|
1905
|
+
taxId: buyerTaxId,
|
|
1906
|
+
name: response.buyer_name || "",
|
|
1907
|
+
address: response.buyer_address || void 0,
|
|
1908
|
+
phone: response.buyer_telephone_number || void 0,
|
|
1909
|
+
email: response.buyer_email_address || void 0
|
|
1910
|
+
});
|
|
1911
|
+
const carrier = this.buildCarrier(
|
|
1912
|
+
response.carrier_type,
|
|
1913
|
+
response.carrier_id1
|
|
1914
|
+
);
|
|
1915
|
+
const donation = response.npoban ? Donation.tryCreate(response.npoban) : Donation.none();
|
|
1916
|
+
const invoice = Invoice.create({
|
|
1917
|
+
orderId: OrderId.create(response.order_id || response.invoice_number),
|
|
1918
|
+
buyer,
|
|
1919
|
+
items,
|
|
1920
|
+
carrier,
|
|
1921
|
+
donation,
|
|
1922
|
+
remark: response.main_remark || void 0
|
|
1923
|
+
});
|
|
1924
|
+
const invoiceNumber = InvoiceNumber.create(response.invoice_number);
|
|
1925
|
+
const invoiceTime = AmegoMapper.parseIsoDate(response.invoice_time);
|
|
1926
|
+
invoice.setInvoiceNumber(invoiceNumber, invoiceTime, response.random_number);
|
|
1927
|
+
invoice.updateStatus(AmegoMapper.toInvoiceStatus(response.invoice_status));
|
|
1928
|
+
return {
|
|
1929
|
+
invoice,
|
|
1930
|
+
status: AmegoMapper.toInvoiceStatus(response.invoice_status)
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Map API list item response to domain query result
|
|
1935
|
+
*/
|
|
1936
|
+
mapListItemResponse(item) {
|
|
1937
|
+
const placeholderItem = InvoiceItem.create({
|
|
1938
|
+
description: "\uFF08\u8A73\u7D30\u8ACB\u67E5\u8A62\u55AE\u5F35\u767C\u7968\uFF09",
|
|
1939
|
+
quantity: 1,
|
|
1940
|
+
unitPrice: Money.create(item.total_amount),
|
|
1941
|
+
amount: Money.create(item.total_amount),
|
|
1942
|
+
taxType: AmegoMapper.fromApiTaxType(item.tax_type)
|
|
1943
|
+
});
|
|
1944
|
+
const buyerTaxId = TaxId.tryCreate(item.buyer_identifier) ?? TaxId.none();
|
|
1945
|
+
const buyer = buyerTaxId.isNone() ? Buyer.anonymous(item.buyer_name || "\u6D88\u8CBB\u8005") : Buyer.company({
|
|
1946
|
+
taxId: buyerTaxId,
|
|
1947
|
+
name: item.buyer_name || "",
|
|
1948
|
+
address: item.buyer_address || void 0,
|
|
1949
|
+
phone: item.buyer_telephone_number || void 0,
|
|
1950
|
+
email: item.buyer_email_address || void 0
|
|
1951
|
+
});
|
|
1952
|
+
const carrier = this.buildCarrier(item.carrier_type, item.carrier_id1);
|
|
1953
|
+
const donation = item.npoban ? Donation.tryCreate(item.npoban) : Donation.none();
|
|
1954
|
+
const invoice = Invoice.create({
|
|
1955
|
+
orderId: OrderId.create(item.order_id || item.invoice_number),
|
|
1956
|
+
buyer,
|
|
1957
|
+
items: [placeholderItem],
|
|
1958
|
+
carrier,
|
|
1959
|
+
donation,
|
|
1960
|
+
remark: item.main_remark || void 0
|
|
1961
|
+
});
|
|
1962
|
+
const invoiceNumber = InvoiceNumber.create(item.invoice_number);
|
|
1963
|
+
const invoiceTime = AmegoMapper.parseIsoDate(item.invoice_time);
|
|
1964
|
+
invoice.setInvoiceNumber(invoiceNumber, invoiceTime, item.random_number);
|
|
1965
|
+
invoice.updateStatus(AmegoMapper.toInvoiceStatus(item.invoice_status));
|
|
1966
|
+
return {
|
|
1967
|
+
invoice,
|
|
1968
|
+
status: AmegoMapper.toInvoiceStatus(item.invoice_status)
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Build Carrier from API response fields
|
|
1973
|
+
*/
|
|
1974
|
+
buildCarrier(carrierType, carrierId) {
|
|
1975
|
+
if (!carrierType || carrierType === "") {
|
|
1976
|
+
return Carrier.none();
|
|
1977
|
+
}
|
|
1978
|
+
switch (carrierType) {
|
|
1979
|
+
case "3J0002" /* MOBILE */:
|
|
1980
|
+
return Carrier.mobile(carrierId);
|
|
1981
|
+
case "CQ0001" /* CERTIFICATE */:
|
|
1982
|
+
return Carrier.certificate(carrierId);
|
|
1983
|
+
default:
|
|
1984
|
+
return Carrier.custom(carrierType, carrierId);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
|
|
1989
|
+
// src/domain/shared/Pagination.ts
|
|
1990
|
+
var Pagination = class _Pagination {
|
|
1991
|
+
constructor(_page, _limit) {
|
|
1992
|
+
this._page = _page;
|
|
1993
|
+
this._limit = _limit;
|
|
1994
|
+
}
|
|
1995
|
+
static DEFAULT_LIMIT = 20;
|
|
1996
|
+
static MIN_LIMIT = 20;
|
|
1997
|
+
static MAX_LIMIT = 500;
|
|
1998
|
+
/**
|
|
1999
|
+
* Create pagination with specified page and limit
|
|
2000
|
+
*/
|
|
2001
|
+
static create(options = {}) {
|
|
2002
|
+
const page = options.page ?? 1;
|
|
2003
|
+
const limit = options.limit ?? _Pagination.DEFAULT_LIMIT;
|
|
2004
|
+
if (page < 1) {
|
|
2005
|
+
throw new ValidationError("page", "Page must be at least 1");
|
|
2006
|
+
}
|
|
2007
|
+
if (limit < _Pagination.MIN_LIMIT || limit > _Pagination.MAX_LIMIT) {
|
|
2008
|
+
throw new ValidationError(
|
|
2009
|
+
"limit",
|
|
2010
|
+
`Limit must be between ${_Pagination.MIN_LIMIT} and ${_Pagination.MAX_LIMIT}`
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
return new _Pagination(page, limit);
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Create first page with default limit
|
|
2017
|
+
*/
|
|
2018
|
+
static first(limit) {
|
|
2019
|
+
return _Pagination.create({ page: 1, limit });
|
|
2020
|
+
}
|
|
2021
|
+
/**
|
|
2022
|
+
* Get the page number (1-based)
|
|
2023
|
+
*/
|
|
2024
|
+
get page() {
|
|
2025
|
+
return this._page;
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Get the limit (items per page)
|
|
2029
|
+
*/
|
|
2030
|
+
get limit() {
|
|
2031
|
+
return this._limit;
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Get the offset (0-based, for SQL queries)
|
|
2035
|
+
*/
|
|
2036
|
+
get offset() {
|
|
2037
|
+
return (this._page - 1) * this._limit;
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Create next page pagination
|
|
2041
|
+
*/
|
|
2042
|
+
next() {
|
|
2043
|
+
return new _Pagination(this._page + 1, this._limit);
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Create previous page pagination (min page 1)
|
|
2047
|
+
*/
|
|
2048
|
+
previous() {
|
|
2049
|
+
return new _Pagination(Math.max(1, this._page - 1), this._limit);
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
function createPaginatedResult(items, totalCount, pagination) {
|
|
2053
|
+
const totalPages = Math.ceil(totalCount / pagination.limit);
|
|
2054
|
+
return {
|
|
2055
|
+
items,
|
|
2056
|
+
totalCount,
|
|
2057
|
+
totalPages,
|
|
2058
|
+
currentPage: pagination.page,
|
|
2059
|
+
pageSize: pagination.limit,
|
|
2060
|
+
hasNextPage: pagination.page < totalPages,
|
|
2061
|
+
hasPreviousPage: pagination.page > 1
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// src/domain/shared/LocalDate.ts
|
|
2066
|
+
var LocalDate = class _LocalDate {
|
|
2067
|
+
constructor(_year, _month, _day) {
|
|
2068
|
+
this._year = _year;
|
|
2069
|
+
this._month = _month;
|
|
2070
|
+
this._day = _day;
|
|
2071
|
+
}
|
|
2072
|
+
/**
|
|
2073
|
+
* Create a LocalDate from year, month, day
|
|
2074
|
+
*/
|
|
2075
|
+
static of(year, month, day) {
|
|
2076
|
+
if (month < 1 || month > 12) {
|
|
2077
|
+
throw new ValidationError("month", "Month must be between 1 and 12");
|
|
2078
|
+
}
|
|
2079
|
+
if (day < 1 || day > 31) {
|
|
2080
|
+
throw new ValidationError("day", "Day must be between 1 and 31");
|
|
2081
|
+
}
|
|
2082
|
+
return new _LocalDate(year, month, day);
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Create a LocalDate from a JavaScript Date
|
|
2086
|
+
*/
|
|
2087
|
+
static fromDate(date) {
|
|
2088
|
+
return new _LocalDate(
|
|
2089
|
+
date.getFullYear(),
|
|
2090
|
+
date.getMonth() + 1,
|
|
2091
|
+
date.getDate()
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Create a LocalDate from YYYYMMDD string or number
|
|
2096
|
+
*/
|
|
2097
|
+
static parse(value) {
|
|
2098
|
+
const str = String(value);
|
|
2099
|
+
if (str.length !== 8) {
|
|
2100
|
+
throw new ValidationError("date", "Date must be in YYYYMMDD format");
|
|
2101
|
+
}
|
|
2102
|
+
const year = parseInt(str.substring(0, 4), 10);
|
|
2103
|
+
const month = parseInt(str.substring(4, 6), 10);
|
|
2104
|
+
const day = parseInt(str.substring(6, 8), 10);
|
|
2105
|
+
return _LocalDate.of(year, month, day);
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Get today's date
|
|
2109
|
+
*/
|
|
2110
|
+
static today() {
|
|
2111
|
+
return _LocalDate.fromDate(/* @__PURE__ */ new Date());
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Get the year
|
|
2115
|
+
*/
|
|
2116
|
+
get year() {
|
|
2117
|
+
return this._year;
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Get the month (1-12)
|
|
2121
|
+
*/
|
|
2122
|
+
get month() {
|
|
2123
|
+
return this._month;
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Get the day of month
|
|
2127
|
+
*/
|
|
2128
|
+
get day() {
|
|
2129
|
+
return this._day;
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Convert to YYYYMMDD number (for API calls)
|
|
2133
|
+
*/
|
|
2134
|
+
toNumber() {
|
|
2135
|
+
return this._year * 1e4 + this._month * 100 + this._day;
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* Convert to YYYYMMDD string
|
|
2139
|
+
*/
|
|
2140
|
+
toString() {
|
|
2141
|
+
const month = String(this._month).padStart(2, "0");
|
|
2142
|
+
const day = String(this._day).padStart(2, "0");
|
|
2143
|
+
return `${this._year}${month}${day}`;
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Convert to JavaScript Date (at midnight)
|
|
2147
|
+
*/
|
|
2148
|
+
toDate() {
|
|
2149
|
+
return new Date(this._year, this._month - 1, this._day);
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Convert to ISO date string (YYYY-MM-DD)
|
|
2153
|
+
*/
|
|
2154
|
+
toISOString() {
|
|
2155
|
+
const month = String(this._month).padStart(2, "0");
|
|
2156
|
+
const day = String(this._day).padStart(2, "0");
|
|
2157
|
+
return `${this._year}-${month}-${day}`;
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Check if this date is before another
|
|
2161
|
+
*/
|
|
2162
|
+
isBefore(other) {
|
|
2163
|
+
return this.toNumber() < other.toNumber();
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
2166
|
+
* Check if this date is after another
|
|
2167
|
+
*/
|
|
2168
|
+
isAfter(other) {
|
|
2169
|
+
return this.toNumber() > other.toNumber();
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Check equality
|
|
2173
|
+
*/
|
|
2174
|
+
equals(other) {
|
|
2175
|
+
return this.toNumber() === other.toNumber();
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
var DateRange = class _DateRange {
|
|
2179
|
+
constructor(_start, _end) {
|
|
2180
|
+
this._start = _start;
|
|
2181
|
+
this._end = _end;
|
|
2182
|
+
}
|
|
2183
|
+
/**
|
|
2184
|
+
* Create a date range between two dates
|
|
2185
|
+
*/
|
|
2186
|
+
static between(start, end) {
|
|
2187
|
+
if (start.isAfter(end)) {
|
|
2188
|
+
throw new ValidationError(
|
|
2189
|
+
"dateRange",
|
|
2190
|
+
"Start date must be before or equal to end date"
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
2193
|
+
return new _DateRange(start, end);
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Create a date range for a single day
|
|
2197
|
+
*/
|
|
2198
|
+
static on(date) {
|
|
2199
|
+
return new _DateRange(date, date);
|
|
2200
|
+
}
|
|
2201
|
+
/**
|
|
2202
|
+
* Get the start date
|
|
2203
|
+
*/
|
|
2204
|
+
get start() {
|
|
2205
|
+
return this._start;
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Get the end date
|
|
2209
|
+
*/
|
|
2210
|
+
get end() {
|
|
2211
|
+
return this._end;
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Check if a date is within this range (inclusive)
|
|
2215
|
+
*/
|
|
2216
|
+
contains(date) {
|
|
2217
|
+
return !date.isBefore(this._start) && !date.isAfter(this._end);
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
var DateType = /* @__PURE__ */ ((DateType2) => {
|
|
2221
|
+
DateType2[DateType2["INVOICE_DATE"] = 1] = "INVOICE_DATE";
|
|
2222
|
+
DateType2[DateType2["CREATE_DATE"] = 2] = "CREATE_DATE";
|
|
2223
|
+
return DateType2;
|
|
2224
|
+
})(DateType || {});
|
|
2225
|
+
|
|
2226
|
+
// src/infrastructure/amego/AmegoInvoiceService.ts
|
|
2227
|
+
var AmegoInvoiceService = class {
|
|
2228
|
+
repository;
|
|
2229
|
+
constructor(config) {
|
|
2230
|
+
this.repository = new AmegoInvoiceRepository(config);
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Issue a new invoice (開立發票)
|
|
2234
|
+
*/
|
|
2235
|
+
async issue(invoice) {
|
|
2236
|
+
const result = await this.repository.issue(invoice);
|
|
2237
|
+
return {
|
|
2238
|
+
invoiceNumber: result.invoiceNumber,
|
|
2239
|
+
invoiceTime: result.invoiceTime,
|
|
2240
|
+
randomNumber: result.randomNumber,
|
|
2241
|
+
barcode: result.barcode,
|
|
2242
|
+
qrcodeLeft: result.qrcodeLeft,
|
|
2243
|
+
qrcodeRight: result.qrcodeRight,
|
|
2244
|
+
printData: result.printData
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
/**
|
|
2248
|
+
* Void an invoice (作廢發票)
|
|
2249
|
+
*/
|
|
2250
|
+
async void(invoiceNumber) {
|
|
2251
|
+
await this.repository.void(invoiceNumber);
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* Find invoice by invoice number
|
|
2255
|
+
*/
|
|
2256
|
+
async findByNumber(invoiceNumber) {
|
|
2257
|
+
const result = await this.repository.findByInvoiceNumber(invoiceNumber);
|
|
2258
|
+
return result?.invoice ?? null;
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Find invoice by order ID
|
|
2262
|
+
*/
|
|
2263
|
+
async findByOrderId(orderId) {
|
|
2264
|
+
const result = await this.repository.findByOrderId(orderId);
|
|
2265
|
+
return result?.invoice ?? null;
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* List invoices with query
|
|
2269
|
+
*/
|
|
2270
|
+
async list(query) {
|
|
2271
|
+
const result = await this.repository.list({
|
|
2272
|
+
dateType: query.dateType === 2 /* CREATE_DATE */ ? 2 : 1,
|
|
2273
|
+
startDate: query.dateRange.start.toNumber().toString(),
|
|
2274
|
+
endDate: query.dateRange.end.toNumber().toString(),
|
|
2275
|
+
limit: query.pagination.limit,
|
|
2276
|
+
page: query.pagination.page
|
|
2277
|
+
});
|
|
2278
|
+
const invoices = result.invoices.map((r) => r.invoice);
|
|
2279
|
+
return createPaginatedResult(invoices, result.totalCount, query.pagination);
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Get invoice status
|
|
2283
|
+
*/
|
|
2284
|
+
async getStatus(invoiceNumbers) {
|
|
2285
|
+
const results = await this.repository.getStatus(invoiceNumbers);
|
|
2286
|
+
return results.map((r) => ({
|
|
2287
|
+
invoiceNumber: r.invoiceNumber,
|
|
2288
|
+
type: r.type,
|
|
2289
|
+
status: r.status,
|
|
2290
|
+
totalAmount: r.totalAmount
|
|
2291
|
+
}));
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
|
|
2295
|
+
// src/domain/allowance/AllowanceItem.ts
|
|
2296
|
+
var AllowanceItem = class _AllowanceItem {
|
|
2297
|
+
constructor(_originalDescription, _quantity, _unitPrice, _amount, _unit, _taxType) {
|
|
2298
|
+
this._originalDescription = _originalDescription;
|
|
2299
|
+
this._quantity = _quantity;
|
|
2300
|
+
this._unitPrice = _unitPrice;
|
|
2301
|
+
this._amount = _amount;
|
|
2302
|
+
this._unit = _unit;
|
|
2303
|
+
this._taxType = _taxType;
|
|
2304
|
+
}
|
|
2305
|
+
/**
|
|
2306
|
+
* Create an AllowanceItem
|
|
2307
|
+
*/
|
|
2308
|
+
static create(props) {
|
|
2309
|
+
if (!props.originalDescription || props.originalDescription.trim().length === 0) {
|
|
2310
|
+
throw new ValidationError(
|
|
2311
|
+
"originalDescription",
|
|
2312
|
+
"Original description is required"
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
if (props.originalDescription.length > 256) {
|
|
2316
|
+
throw new ValidationError(
|
|
2317
|
+
"originalDescription",
|
|
2318
|
+
"Original description must not exceed 256 characters"
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
if (props.quantity === 0) {
|
|
2322
|
+
throw new ValidationError("quantity", "Quantity cannot be zero");
|
|
2323
|
+
}
|
|
2324
|
+
if (props.unit && props.unit.length > 6) {
|
|
2325
|
+
throw new ValidationError("unit", "Unit must not exceed 6 characters");
|
|
2326
|
+
}
|
|
2327
|
+
return new _AllowanceItem(
|
|
2328
|
+
props.originalDescription.trim(),
|
|
2329
|
+
props.quantity,
|
|
2330
|
+
props.unitPrice,
|
|
2331
|
+
props.amount,
|
|
2332
|
+
props.unit?.trim() ?? "",
|
|
2333
|
+
props.taxType ?? 1 /* TAXABLE */
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* Create an AllowanceItem with auto-calculated amount
|
|
2338
|
+
*/
|
|
2339
|
+
static createWithAutoAmount(props) {
|
|
2340
|
+
const amount = props.unitPrice.multiply(props.quantity);
|
|
2341
|
+
return _AllowanceItem.create({ ...props, amount });
|
|
2342
|
+
}
|
|
2343
|
+
// Getters
|
|
2344
|
+
get originalDescription() {
|
|
2345
|
+
return this._originalDescription;
|
|
2346
|
+
}
|
|
2347
|
+
get quantity() {
|
|
2348
|
+
return this._quantity;
|
|
2349
|
+
}
|
|
2350
|
+
get unitPrice() {
|
|
2351
|
+
return this._unitPrice;
|
|
2352
|
+
}
|
|
2353
|
+
get amount() {
|
|
2354
|
+
return this._amount;
|
|
2355
|
+
}
|
|
2356
|
+
get unit() {
|
|
2357
|
+
return this._unit;
|
|
2358
|
+
}
|
|
2359
|
+
get taxType() {
|
|
2360
|
+
return this._taxType;
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
|
|
2364
|
+
// src/infrastructure/amego/AmegoAllowanceRepository.ts
|
|
2365
|
+
var allowanceCounter = 0;
|
|
2366
|
+
function generateAllowanceNumber() {
|
|
2367
|
+
const now = /* @__PURE__ */ new Date();
|
|
2368
|
+
const datePart = AmegoMapper.formatDate(now);
|
|
2369
|
+
const seq = String(++allowanceCounter % 1e6).padStart(6, "0");
|
|
2370
|
+
return `AL${datePart}${seq}`;
|
|
2371
|
+
}
|
|
2372
|
+
var AmegoAllowanceRepository = class {
|
|
2373
|
+
client;
|
|
2374
|
+
constructor(config) {
|
|
2375
|
+
this.client = new AmegoClient(config);
|
|
2376
|
+
}
|
|
2377
|
+
/**
|
|
2378
|
+
* Issue a new allowance (開立折讓)
|
|
2379
|
+
*
|
|
2380
|
+
* 使用 /json/g0401 endpoint
|
|
2381
|
+
*/
|
|
2382
|
+
async issue(allowance) {
|
|
2383
|
+
const allowanceNumber = generateAllowanceNumber();
|
|
2384
|
+
const payload = AmegoMapper.toAllowancePayload(allowance, allowanceNumber);
|
|
2385
|
+
await this.client.post(
|
|
2386
|
+
AMEGO_ENDPOINTS.ALLOWANCE_ISSUE,
|
|
2387
|
+
payload
|
|
2388
|
+
);
|
|
2389
|
+
const allowanceDate = AmegoMapper.parseDate(payload.AllowanceDate);
|
|
2390
|
+
allowance.setAllowanceNumber(allowanceNumber, allowanceDate);
|
|
2391
|
+
allowance.updateStatus(3 /* UPLOADED */);
|
|
2392
|
+
return {
|
|
2393
|
+
allowanceNumber,
|
|
2394
|
+
allowanceDate
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* Void an allowance (作廢折讓)
|
|
2399
|
+
*
|
|
2400
|
+
* 使用 /json/g0501 endpoint
|
|
2401
|
+
*/
|
|
2402
|
+
async void(allowanceNumber) {
|
|
2403
|
+
const payload = {
|
|
2404
|
+
CancelAllowanceNumber: allowanceNumber
|
|
2405
|
+
};
|
|
2406
|
+
await this.client.post(AMEGO_ENDPOINTS.ALLOWANCE_VOID, payload);
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Query allowance by allowance number (查詢折讓 - 依折讓單編號)
|
|
2410
|
+
*
|
|
2411
|
+
* 使用 /json/allowance_query endpoint
|
|
2412
|
+
*/
|
|
2413
|
+
async findByAllowanceNumber(allowanceNumber) {
|
|
2414
|
+
try {
|
|
2415
|
+
const payload = {
|
|
2416
|
+
type: "allowance",
|
|
2417
|
+
allowance_number: allowanceNumber
|
|
2418
|
+
};
|
|
2419
|
+
const response = await this.client.post(
|
|
2420
|
+
AMEGO_ENDPOINTS.ALLOWANCE_QUERY,
|
|
2421
|
+
payload
|
|
2422
|
+
);
|
|
2423
|
+
if (!response.data) {
|
|
2424
|
+
return null;
|
|
2425
|
+
}
|
|
2426
|
+
return this.mapQueryResponse(response.data);
|
|
2427
|
+
} catch {
|
|
2428
|
+
return null;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Query allowances by original invoice number (查詢折讓 - 依原發票號碼)
|
|
2433
|
+
*
|
|
2434
|
+
* 使用 /json/allowance_query endpoint
|
|
2435
|
+
*/
|
|
2436
|
+
async findByInvoiceNumber(invoiceNumber) {
|
|
2437
|
+
try {
|
|
2438
|
+
const payload = {
|
|
2439
|
+
type: "invoice",
|
|
2440
|
+
invoice_number: invoiceNumber.toString()
|
|
2441
|
+
};
|
|
2442
|
+
const response = await this.client.post(
|
|
2443
|
+
AMEGO_ENDPOINTS.ALLOWANCE_QUERY,
|
|
2444
|
+
payload
|
|
2445
|
+
);
|
|
2446
|
+
if (!response.data) {
|
|
2447
|
+
return [];
|
|
2448
|
+
}
|
|
2449
|
+
return [this.mapQueryResponse(response.data)];
|
|
2450
|
+
} catch {
|
|
2451
|
+
return [];
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Check allowance status (查詢折讓狀態)
|
|
2456
|
+
*
|
|
2457
|
+
* 使用 /json/allowance_status endpoint
|
|
2458
|
+
*/
|
|
2459
|
+
async getStatus(allowanceNumbers) {
|
|
2460
|
+
const results = [];
|
|
2461
|
+
for (const allowanceNumber of allowanceNumbers) {
|
|
2462
|
+
try {
|
|
2463
|
+
const payload = {
|
|
2464
|
+
AllowanceNumber: allowanceNumber
|
|
2465
|
+
};
|
|
2466
|
+
const response = await this.client.post(
|
|
2467
|
+
AMEGO_ENDPOINTS.ALLOWANCE_STATUS,
|
|
2468
|
+
payload
|
|
2469
|
+
);
|
|
2470
|
+
if (response.data && response.data.length > 0) {
|
|
2471
|
+
for (const item of response.data) {
|
|
2472
|
+
results.push({
|
|
2473
|
+
allowanceNumber: item.allowance_number,
|
|
2474
|
+
status: AmegoMapper.toAllowanceStatus(item.status),
|
|
2475
|
+
totalAmount: item.total_amount
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
} catch {
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
return results;
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* List allowances (折讓列表)
|
|
2486
|
+
*
|
|
2487
|
+
* 使用 /json/allowance_list endpoint
|
|
2488
|
+
*/
|
|
2489
|
+
async list(options) {
|
|
2490
|
+
const dateStart = parseInt(options.startDate, 10);
|
|
2491
|
+
const dateEnd = parseInt(options.endDate, 10);
|
|
2492
|
+
const payload = {
|
|
2493
|
+
date_select: options.dateType,
|
|
2494
|
+
date_start: dateStart,
|
|
2495
|
+
date_end: dateEnd,
|
|
2496
|
+
limit: options.limit ?? 20,
|
|
2497
|
+
page: options.page ?? 1
|
|
2498
|
+
};
|
|
2499
|
+
const response = await this.client.post(
|
|
2500
|
+
AMEGO_ENDPOINTS.ALLOWANCE_LIST,
|
|
2501
|
+
payload
|
|
2502
|
+
);
|
|
2503
|
+
const allowances = (response.data ?? []).map(
|
|
2504
|
+
(item) => this.mapListItemResponse(item)
|
|
2505
|
+
);
|
|
2506
|
+
return {
|
|
2507
|
+
allowances,
|
|
2508
|
+
totalPages: response.page_total ?? 1,
|
|
2509
|
+
currentPage: response.page_now ?? 1,
|
|
2510
|
+
totalCount: response.data_total ?? 0
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
/**
|
|
2514
|
+
* Map API query response to domain query result
|
|
2515
|
+
*/
|
|
2516
|
+
mapQueryResponse(data) {
|
|
2517
|
+
const items = (data.product_item ?? []).map(
|
|
2518
|
+
(item) => AllowanceItem.create({
|
|
2519
|
+
originalDescription: item.description,
|
|
2520
|
+
quantity: item.quantity,
|
|
2521
|
+
unitPrice: Money.create(item.unit_price),
|
|
2522
|
+
amount: Money.create(item.amount),
|
|
2523
|
+
unit: item.unit || void 0,
|
|
2524
|
+
taxType: AmegoMapper.fromApiTaxType(item.tax_type)
|
|
2525
|
+
})
|
|
2526
|
+
);
|
|
2527
|
+
const firstItem = data.product_item?.[0];
|
|
2528
|
+
const originalInvoiceNumber = firstItem?.original_invoice_number ?? "";
|
|
2529
|
+
const originalInvoiceDate = firstItem?.original_invoice_date ? AmegoMapper.parseDate(firstItem.original_invoice_date) : /* @__PURE__ */ new Date();
|
|
2530
|
+
const allowance = Allowance.create({
|
|
2531
|
+
originalInvoiceNumber: InvoiceNumber.create(originalInvoiceNumber),
|
|
2532
|
+
originalInvoiceDate,
|
|
2533
|
+
buyerTaxId: TaxId.tryCreate(data.buyer_identifier) ?? TaxId.none(),
|
|
2534
|
+
sellerTaxId: TaxId.none(),
|
|
2535
|
+
// API 回應沒有賣方資訊,使用空值
|
|
2536
|
+
buyerName: data.buyer_name || "",
|
|
2537
|
+
items
|
|
2538
|
+
});
|
|
2539
|
+
const allowanceDate = AmegoMapper.parseDate(data.allowance_date);
|
|
2540
|
+
allowance.setAllowanceNumber(data.allowance_number, allowanceDate);
|
|
2541
|
+
allowance.updateStatus(AmegoMapper.toAllowanceStatus(data.invoice_status));
|
|
2542
|
+
return {
|
|
2543
|
+
allowance,
|
|
2544
|
+
status: AmegoMapper.toAllowanceStatus(data.invoice_status)
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Map API list item response to domain query result
|
|
2549
|
+
*/
|
|
2550
|
+
mapListItemResponse(item) {
|
|
2551
|
+
const firstProductItem = item.product_item?.[0];
|
|
2552
|
+
const originalInvoiceNumber = firstProductItem?.original_invoice_number ?? "";
|
|
2553
|
+
const originalInvoiceDate = firstProductItem?.original_invoice_date ? AmegoMapper.parseDate(firstProductItem.original_invoice_date) : /* @__PURE__ */ new Date();
|
|
2554
|
+
const items = (item.product_item ?? []).map(
|
|
2555
|
+
(productItem) => AllowanceItem.create({
|
|
2556
|
+
originalDescription: productItem.description,
|
|
2557
|
+
quantity: productItem.quantity,
|
|
2558
|
+
unitPrice: Money.create(productItem.unit_price),
|
|
2559
|
+
amount: Money.create(productItem.amount),
|
|
2560
|
+
unit: productItem.unit || void 0,
|
|
2561
|
+
taxType: AmegoMapper.fromApiTaxType(productItem.tax_type)
|
|
2562
|
+
})
|
|
2563
|
+
);
|
|
2564
|
+
const allowanceItems = items.length > 0 ? items : [
|
|
2565
|
+
AllowanceItem.create({
|
|
2566
|
+
originalDescription: "\uFF08\u8A73\u7D30\u8ACB\u67E5\u8A62\u55AE\u5F35\u6298\u8B93\uFF09",
|
|
2567
|
+
quantity: 1,
|
|
2568
|
+
unitPrice: Money.create(item.total_amount),
|
|
2569
|
+
amount: Money.create(item.total_amount),
|
|
2570
|
+
taxType: AmegoMapper.fromApiTaxType(1)
|
|
2571
|
+
// Default to taxable
|
|
2572
|
+
})
|
|
2573
|
+
];
|
|
2574
|
+
const allowance = Allowance.create({
|
|
2575
|
+
originalInvoiceNumber: InvoiceNumber.create(originalInvoiceNumber),
|
|
2576
|
+
originalInvoiceDate,
|
|
2577
|
+
buyerTaxId: TaxId.tryCreate(item.buyer_identifier) ?? TaxId.none(),
|
|
2578
|
+
sellerTaxId: TaxId.none(),
|
|
2579
|
+
buyerName: item.buyer_name || "",
|
|
2580
|
+
items: allowanceItems
|
|
2581
|
+
});
|
|
2582
|
+
const allowanceDate = AmegoMapper.parseDate(item.allowance_date);
|
|
2583
|
+
allowance.setAllowanceNumber(item.allowance_number, allowanceDate);
|
|
2584
|
+
allowance.updateStatus(AmegoMapper.toAllowanceStatus(item.invoice_status));
|
|
2585
|
+
return {
|
|
2586
|
+
allowance,
|
|
2587
|
+
status: AmegoMapper.toAllowanceStatus(item.invoice_status)
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
|
|
2592
|
+
// src/infrastructure/amego/AmegoAllowanceService.ts
|
|
2593
|
+
var AmegoAllowanceService = class {
|
|
2594
|
+
repository;
|
|
2595
|
+
constructor(config) {
|
|
2596
|
+
this.repository = new AmegoAllowanceRepository(config);
|
|
2597
|
+
}
|
|
2598
|
+
/**
|
|
2599
|
+
* Issue a new allowance (開立折讓)
|
|
2600
|
+
*/
|
|
2601
|
+
async issue(allowance) {
|
|
2602
|
+
const result = await this.repository.issue(allowance);
|
|
2603
|
+
return {
|
|
2604
|
+
allowanceNumber: result.allowanceNumber,
|
|
2605
|
+
allowanceDate: result.allowanceDate
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Void an allowance (作廢折讓)
|
|
2610
|
+
*/
|
|
2611
|
+
async void(allowanceNumber) {
|
|
2612
|
+
await this.repository.void(allowanceNumber);
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* Find allowance by allowance number
|
|
2616
|
+
*/
|
|
2617
|
+
async findByNumber(allowanceNumber) {
|
|
2618
|
+
const result = await this.repository.findByAllowanceNumber(allowanceNumber);
|
|
2619
|
+
return result?.allowance ?? null;
|
|
2620
|
+
}
|
|
2621
|
+
/**
|
|
2622
|
+
* Find allowances by original invoice number
|
|
2623
|
+
*/
|
|
2624
|
+
async findByInvoiceNumber(invoiceNumber) {
|
|
2625
|
+
const results = await this.repository.findByInvoiceNumber(invoiceNumber);
|
|
2626
|
+
return results.map((r) => r.allowance);
|
|
2627
|
+
}
|
|
2628
|
+
/**
|
|
2629
|
+
* List allowances with query
|
|
2630
|
+
*/
|
|
2631
|
+
async list(query) {
|
|
2632
|
+
const result = await this.repository.list({
|
|
2633
|
+
dateType: query.dateType === 2 /* CREATE_DATE */ ? 2 : 1,
|
|
2634
|
+
startDate: query.dateRange.start.toNumber().toString(),
|
|
2635
|
+
endDate: query.dateRange.end.toNumber().toString(),
|
|
2636
|
+
limit: query.pagination.limit,
|
|
2637
|
+
page: query.pagination.page
|
|
2638
|
+
});
|
|
2639
|
+
const allowances = result.allowances.map((r) => r.allowance);
|
|
2640
|
+
return createPaginatedResult(allowances, result.totalCount, query.pagination);
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Get allowance status
|
|
2644
|
+
*/
|
|
2645
|
+
async getStatus(allowanceNumbers) {
|
|
2646
|
+
const results = await this.repository.getStatus(allowanceNumbers);
|
|
2647
|
+
return results.map((r) => ({
|
|
2648
|
+
allowanceNumber: r.allowanceNumber,
|
|
2649
|
+
status: r.status,
|
|
2650
|
+
totalAmount: r.totalAmount
|
|
2651
|
+
}));
|
|
2652
|
+
}
|
|
2653
|
+
};
|
|
2654
|
+
|
|
2655
|
+
// src/Zinvoice.ts
|
|
2656
|
+
var Zinvoice = class _Zinvoice {
|
|
2657
|
+
_provider;
|
|
2658
|
+
_invoiceService;
|
|
2659
|
+
_allowanceService;
|
|
2660
|
+
_capabilities;
|
|
2661
|
+
constructor(provider, invoiceService, allowanceService) {
|
|
2662
|
+
this._provider = provider;
|
|
2663
|
+
this._invoiceService = invoiceService;
|
|
2664
|
+
this._allowanceService = allowanceService;
|
|
2665
|
+
this._capabilities = PROVIDER_CAPABILITIES[provider];
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Create a Zinvoice client
|
|
2669
|
+
*/
|
|
2670
|
+
static create(config) {
|
|
2671
|
+
switch (config.provider) {
|
|
2672
|
+
case "amego" /* AMEGO */:
|
|
2673
|
+
return _Zinvoice.createAmego(config);
|
|
2674
|
+
default:
|
|
2675
|
+
throw new ProviderNotImplementedError(config.provider);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
/**
|
|
2679
|
+
* Create a Zinvoice client for Amego provider
|
|
2680
|
+
*/
|
|
2681
|
+
static createAmego(config) {
|
|
2682
|
+
const amegoConfig = {
|
|
2683
|
+
baseUrl: config.baseUrl ?? "https://invoice-api.amego.tw",
|
|
2684
|
+
sellerTaxId: config.sellerTaxId,
|
|
2685
|
+
apiKey: config.apiKey,
|
|
2686
|
+
timeout: config.timeout
|
|
2687
|
+
};
|
|
2688
|
+
const invoiceService = new AmegoInvoiceService(amegoConfig);
|
|
2689
|
+
const allowanceService = new AmegoAllowanceService(amegoConfig);
|
|
2690
|
+
return new _Zinvoice("amego" /* AMEGO */, invoiceService, allowanceService);
|
|
2691
|
+
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Get the invoice service
|
|
2694
|
+
*/
|
|
2695
|
+
get invoices() {
|
|
2696
|
+
return this._invoiceService;
|
|
2697
|
+
}
|
|
2698
|
+
/**
|
|
2699
|
+
* Get the allowance service
|
|
2700
|
+
*/
|
|
2701
|
+
get allowances() {
|
|
2702
|
+
return this._allowanceService;
|
|
2703
|
+
}
|
|
2704
|
+
/**
|
|
2705
|
+
* Get the current provider
|
|
2706
|
+
*/
|
|
2707
|
+
get provider() {
|
|
2708
|
+
return this._provider;
|
|
2709
|
+
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Get the provider display name
|
|
2712
|
+
*/
|
|
2713
|
+
get providerName() {
|
|
2714
|
+
return PROVIDER_NAMES[this._provider];
|
|
2715
|
+
}
|
|
2716
|
+
/**
|
|
2717
|
+
* Check if a capability is supported
|
|
2718
|
+
*/
|
|
2719
|
+
supports(capability) {
|
|
2720
|
+
return this._capabilities.has(capability);
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* Get all supported capabilities
|
|
2724
|
+
*/
|
|
2725
|
+
getCapabilities() {
|
|
2726
|
+
return Array.from(this._capabilities);
|
|
2727
|
+
}
|
|
2728
|
+
/**
|
|
2729
|
+
* Assert that a capability is supported, throw if not
|
|
2730
|
+
*/
|
|
2731
|
+
requireCapability(capability) {
|
|
2732
|
+
if (!this.supports(capability)) {
|
|
2733
|
+
throw new UnsupportedCapabilityError(capability, this._provider);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
};
|
|
2737
|
+
|
|
2738
|
+
// src/domain/shared/CarrierCode.ts
|
|
2739
|
+
var CarrierType2 = /* @__PURE__ */ ((CarrierType3) => {
|
|
2740
|
+
CarrierType3["MOBILE"] = "3J0002";
|
|
2741
|
+
CarrierType3["CERTIFICATE"] = "CQ0001";
|
|
2742
|
+
CarrierType3["AMEGO"] = "amego";
|
|
2743
|
+
CarrierType3["NONE"] = "";
|
|
2744
|
+
return CarrierType3;
|
|
2745
|
+
})(CarrierType2 || {});
|
|
2746
|
+
var CarrierCode = class _CarrierCode {
|
|
2747
|
+
constructor(_type, _code1, _code2) {
|
|
2748
|
+
this._type = _type;
|
|
2749
|
+
this._code1 = _code1;
|
|
2750
|
+
this._code2 = _code2;
|
|
2751
|
+
}
|
|
2752
|
+
/**
|
|
2753
|
+
* Create a CarrierCode for mobile barcode
|
|
2754
|
+
* Format: /[A-Z0-9.+-]{7}
|
|
2755
|
+
*/
|
|
2756
|
+
static mobile(code) {
|
|
2757
|
+
const normalized = code.trim().toUpperCase();
|
|
2758
|
+
if (!_CarrierCode.isValidMobileBarcode(normalized)) {
|
|
2759
|
+
throw new InvalidCarrierCodeError(code, "3J0002" /* MOBILE */);
|
|
2760
|
+
}
|
|
2761
|
+
return new _CarrierCode("3J0002" /* MOBILE */, normalized, normalized);
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Create a CarrierCode for natural person certificate
|
|
2765
|
+
* Format: 2 letters + 14 digits
|
|
2766
|
+
*/
|
|
2767
|
+
static certificate(code) {
|
|
2768
|
+
const normalized = code.trim().toUpperCase();
|
|
2769
|
+
if (!_CarrierCode.isValidCertificate(normalized)) {
|
|
2770
|
+
throw new InvalidCarrierCodeError(code, "CQ0001" /* CERTIFICATE */);
|
|
2771
|
+
}
|
|
2772
|
+
return new _CarrierCode("CQ0001" /* CERTIFICATE */, normalized, normalized);
|
|
2773
|
+
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Create a CarrierCode for Amego member
|
|
2776
|
+
* Format: a+phone number (a0911222333) or email
|
|
2777
|
+
*/
|
|
2778
|
+
static amego(code) {
|
|
2779
|
+
const normalized = code.trim().toLowerCase();
|
|
2780
|
+
if (!_CarrierCode.isValidAmego(normalized)) {
|
|
2781
|
+
throw new InvalidCarrierCodeError(code, "amego" /* AMEGO */);
|
|
2782
|
+
}
|
|
2783
|
+
return new _CarrierCode("amego" /* AMEGO */, normalized, normalized);
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Create a CarrierCode for custom carrier type
|
|
2787
|
+
*/
|
|
2788
|
+
static custom(type, code1, code2) {
|
|
2789
|
+
return new _CarrierCode(type, code1.trim(), code2.trim());
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Create an empty carrier (no carrier)
|
|
2793
|
+
*/
|
|
2794
|
+
static none() {
|
|
2795
|
+
return new _CarrierCode("" /* NONE */, "", "");
|
|
2796
|
+
}
|
|
2797
|
+
/**
|
|
2798
|
+
* Validate mobile barcode format
|
|
2799
|
+
* Format: starts with "/" followed by 7 characters (A-Z, 0-9, +, -, .)
|
|
2800
|
+
*/
|
|
2801
|
+
static isValidMobileBarcode(code) {
|
|
2802
|
+
return /^\/[A-Z0-9.+-]{7}$/.test(code);
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* Validate natural person certificate format
|
|
2806
|
+
* Format: 2 uppercase letters + 14 digits
|
|
2807
|
+
*/
|
|
2808
|
+
static isValidCertificate(code) {
|
|
2809
|
+
return /^[A-Z]{2}\d{14}$/.test(code);
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* Validate Amego member carrier format
|
|
2813
|
+
* Format: "a" + phone number or email
|
|
2814
|
+
*/
|
|
2815
|
+
static isValidAmego(code) {
|
|
2816
|
+
if (/^a09\d{8}$/.test(code)) {
|
|
2817
|
+
return true;
|
|
2818
|
+
}
|
|
2819
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(code)) {
|
|
2820
|
+
return true;
|
|
2821
|
+
}
|
|
2822
|
+
return false;
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Get carrier type (alias for type property)
|
|
2826
|
+
*/
|
|
2827
|
+
getType() {
|
|
2828
|
+
return this._type;
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* Get carrier code 1 (顯碼)
|
|
2832
|
+
*/
|
|
2833
|
+
getCode1() {
|
|
2834
|
+
return this._code1;
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* Get carrier code 2 (隱碼)
|
|
2838
|
+
*/
|
|
2839
|
+
getCode2() {
|
|
2840
|
+
return this._code2;
|
|
2841
|
+
}
|
|
2842
|
+
/**
|
|
2843
|
+
* Carrier type for API usage
|
|
2844
|
+
*/
|
|
2845
|
+
get type() {
|
|
2846
|
+
return this._type;
|
|
2847
|
+
}
|
|
2848
|
+
/**
|
|
2849
|
+
* Carrier value (code1) for API usage
|
|
2850
|
+
*/
|
|
2851
|
+
get value() {
|
|
2852
|
+
return this._code1;
|
|
2853
|
+
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Check if this is an empty carrier
|
|
2856
|
+
*/
|
|
2857
|
+
isEmpty() {
|
|
2858
|
+
return this._type === "" /* NONE */;
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* Check if this is a mobile barcode carrier
|
|
2862
|
+
*/
|
|
2863
|
+
isMobile() {
|
|
2864
|
+
return this._type === "3J0002" /* MOBILE */;
|
|
2865
|
+
}
|
|
2866
|
+
/**
|
|
2867
|
+
* Check equality
|
|
2868
|
+
*/
|
|
2869
|
+
equals(other) {
|
|
2870
|
+
return this._type === other._type && this._code1 === other._code1 && this._code2 === other._code2;
|
|
2871
|
+
}
|
|
2872
|
+
};
|
|
2873
|
+
|
|
2874
|
+
// src/domain/invoice/InvoiceQuery.ts
|
|
2875
|
+
var InvoiceQuery = class _InvoiceQuery {
|
|
2876
|
+
constructor(_dateRange, _dateType, _pagination) {
|
|
2877
|
+
this._dateRange = _dateRange;
|
|
2878
|
+
this._dateType = _dateType;
|
|
2879
|
+
this._pagination = _pagination;
|
|
2880
|
+
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Create a query with all parameters
|
|
2883
|
+
*/
|
|
2884
|
+
static create(options) {
|
|
2885
|
+
return new _InvoiceQuery(
|
|
2886
|
+
options.dateRange,
|
|
2887
|
+
options.dateType ?? 1 /* INVOICE_DATE */,
|
|
2888
|
+
options.pagination ?? Pagination.first()
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
/**
|
|
2892
|
+
* Create a query by invoice date
|
|
2893
|
+
*/
|
|
2894
|
+
static byInvoiceDate(dateRange, pagination) {
|
|
2895
|
+
return _InvoiceQuery.create({
|
|
2896
|
+
dateRange,
|
|
2897
|
+
dateType: 1 /* INVOICE_DATE */,
|
|
2898
|
+
pagination
|
|
2899
|
+
});
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* Create a query by create date
|
|
2903
|
+
*/
|
|
2904
|
+
static byCreateDate(dateRange, pagination) {
|
|
2905
|
+
return _InvoiceQuery.create({
|
|
2906
|
+
dateRange,
|
|
2907
|
+
dateType: 2 /* CREATE_DATE */,
|
|
2908
|
+
pagination
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
/**
|
|
2912
|
+
* Create a query for a specific month
|
|
2913
|
+
*/
|
|
2914
|
+
static forMonth(year, month, pagination) {
|
|
2915
|
+
const start = LocalDate.of(year, month, 1);
|
|
2916
|
+
const nextMonth = month === 12 ? 1 : month + 1;
|
|
2917
|
+
const nextYear = month === 12 ? year + 1 : year;
|
|
2918
|
+
const lastDay = new Date(nextYear, nextMonth - 1, 0).getDate();
|
|
2919
|
+
const end = LocalDate.of(year, month, lastDay);
|
|
2920
|
+
return _InvoiceQuery.create({
|
|
2921
|
+
dateRange: DateRange.between(start, end),
|
|
2922
|
+
dateType: 1 /* INVOICE_DATE */,
|
|
2923
|
+
pagination
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
/**
|
|
2927
|
+
* Create a query for a specific year
|
|
2928
|
+
*/
|
|
2929
|
+
static forYear(year, pagination) {
|
|
2930
|
+
return _InvoiceQuery.create({
|
|
2931
|
+
dateRange: DateRange.between(
|
|
2932
|
+
LocalDate.of(year, 1, 1),
|
|
2933
|
+
LocalDate.of(year, 12, 31)
|
|
2934
|
+
),
|
|
2935
|
+
dateType: 1 /* INVOICE_DATE */,
|
|
2936
|
+
pagination
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
/**
|
|
2940
|
+
* Get the date range
|
|
2941
|
+
*/
|
|
2942
|
+
get dateRange() {
|
|
2943
|
+
return this._dateRange;
|
|
2944
|
+
}
|
|
2945
|
+
/**
|
|
2946
|
+
* Get the date type
|
|
2947
|
+
*/
|
|
2948
|
+
get dateType() {
|
|
2949
|
+
return this._dateType;
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* Get the pagination
|
|
2953
|
+
*/
|
|
2954
|
+
get pagination() {
|
|
2955
|
+
return this._pagination;
|
|
2956
|
+
}
|
|
2957
|
+
/**
|
|
2958
|
+
* Create a new query with different pagination
|
|
2959
|
+
*/
|
|
2960
|
+
withPagination(pagination) {
|
|
2961
|
+
return new _InvoiceQuery(this._dateRange, this._dateType, pagination);
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2964
|
+
* Create a query for the next page
|
|
2965
|
+
*/
|
|
2966
|
+
nextPage() {
|
|
2967
|
+
return this.withPagination(this._pagination.next());
|
|
2968
|
+
}
|
|
2969
|
+
};
|
|
2970
|
+
|
|
2971
|
+
// src/domain/services/TaxCalculator.ts
|
|
2972
|
+
var TaxCalculator = class _TaxCalculator {
|
|
2973
|
+
/**
|
|
2974
|
+
* Calculate tax for invoice items
|
|
2975
|
+
*/
|
|
2976
|
+
static calculateForInvoice(items, pricesIncludeTax, hasB2BTaxId) {
|
|
2977
|
+
const taxableSales = _TaxCalculator.sumByTaxType(items, 1 /* TAXABLE */);
|
|
2978
|
+
const taxExemptSales = _TaxCalculator.sumByTaxType(items, 3 /* TAX_EXEMPT */);
|
|
2979
|
+
const zeroRatedSales = _TaxCalculator.sumByTaxType(items, 2 /* ZERO_RATED */);
|
|
2980
|
+
const taxAmount = hasB2BTaxId ? _TaxCalculator.calculateTaxAmount(taxableSales, pricesIncludeTax) : Money.zero();
|
|
2981
|
+
const totalAmount = _TaxCalculator.calculateTotal(
|
|
2982
|
+
taxableSales,
|
|
2983
|
+
taxExemptSales,
|
|
2984
|
+
zeroRatedSales,
|
|
2985
|
+
taxAmount,
|
|
2986
|
+
pricesIncludeTax
|
|
2987
|
+
);
|
|
2988
|
+
return {
|
|
2989
|
+
taxableSales,
|
|
2990
|
+
taxExemptSales,
|
|
2991
|
+
zeroRatedSales,
|
|
2992
|
+
taxAmount,
|
|
2993
|
+
totalAmount
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
/**
|
|
2997
|
+
* Calculate tax for allowance items
|
|
2998
|
+
*/
|
|
2999
|
+
static calculateForAllowance(items, pricesIncludeTax) {
|
|
3000
|
+
const taxableSales = _TaxCalculator.sumAllowanceByTaxType(
|
|
3001
|
+
items,
|
|
3002
|
+
1 /* TAXABLE */
|
|
3003
|
+
);
|
|
3004
|
+
const taxExemptSales = _TaxCalculator.sumAllowanceByTaxType(
|
|
3005
|
+
items,
|
|
3006
|
+
3 /* TAX_EXEMPT */
|
|
3007
|
+
);
|
|
3008
|
+
const zeroRatedSales = _TaxCalculator.sumAllowanceByTaxType(
|
|
3009
|
+
items,
|
|
3010
|
+
2 /* ZERO_RATED */
|
|
3011
|
+
);
|
|
3012
|
+
const taxAmount = _TaxCalculator.calculateTaxAmount(
|
|
3013
|
+
taxableSales,
|
|
3014
|
+
pricesIncludeTax
|
|
3015
|
+
);
|
|
3016
|
+
const totalAmount = _TaxCalculator.calculateTotal(
|
|
3017
|
+
taxableSales,
|
|
3018
|
+
taxExemptSales,
|
|
3019
|
+
zeroRatedSales,
|
|
3020
|
+
taxAmount,
|
|
3021
|
+
pricesIncludeTax
|
|
3022
|
+
);
|
|
3023
|
+
return {
|
|
3024
|
+
taxableSales,
|
|
3025
|
+
taxExemptSales,
|
|
3026
|
+
zeroRatedSales,
|
|
3027
|
+
taxAmount,
|
|
3028
|
+
totalAmount
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
/**
|
|
3032
|
+
* Calculate tax amount from taxable sales
|
|
3033
|
+
*/
|
|
3034
|
+
static calculateTaxAmount(taxableSales, pricesIncludeTax) {
|
|
3035
|
+
if (pricesIncludeTax) {
|
|
3036
|
+
const beforeTax = taxableSales.divide(1 + TAX_RATE).round();
|
|
3037
|
+
return taxableSales.subtract(beforeTax);
|
|
3038
|
+
} else {
|
|
3039
|
+
return taxableSales.multiply(TAX_RATE).round();
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
/**
|
|
3043
|
+
* Calculate net amount (before tax) from tax-included amount
|
|
3044
|
+
*/
|
|
3045
|
+
static extractNetAmount(taxIncludedAmount) {
|
|
3046
|
+
return taxIncludedAmount.divide(1 + TAX_RATE).round();
|
|
3047
|
+
}
|
|
3048
|
+
/**
|
|
3049
|
+
* Calculate gross amount (after tax) from tax-excluded amount
|
|
3050
|
+
*/
|
|
3051
|
+
static addTax(taxExcludedAmount) {
|
|
3052
|
+
const tax = taxExcludedAmount.multiply(TAX_RATE).round();
|
|
3053
|
+
return taxExcludedAmount.add(tax);
|
|
3054
|
+
}
|
|
3055
|
+
/**
|
|
3056
|
+
* Sum invoice items by tax type
|
|
3057
|
+
*/
|
|
3058
|
+
static sumByTaxType(items, taxType) {
|
|
3059
|
+
return items.filter((item) => item.taxType === taxType).reduce((sum, item) => sum.add(item.amount), Money.zero()).round();
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Sum allowance items by tax type
|
|
3063
|
+
*/
|
|
3064
|
+
static sumAllowanceByTaxType(items, taxType) {
|
|
3065
|
+
return items.filter((item) => item.taxType === taxType).reduce((sum, item) => sum.add(item.amount), Money.zero()).round();
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* Calculate total amount
|
|
3069
|
+
*/
|
|
3070
|
+
static calculateTotal(taxableSales, taxExemptSales, zeroRatedSales, taxAmount, pricesIncludeTax) {
|
|
3071
|
+
if (pricesIncludeTax) {
|
|
3072
|
+
return taxableSales.add(taxExemptSales).add(zeroRatedSales);
|
|
3073
|
+
} else {
|
|
3074
|
+
return taxableSales.add(taxExemptSales).add(zeroRatedSales).add(taxAmount);
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
};
|
|
3078
|
+
|
|
3079
|
+
// src/application/InvoiceService.ts
|
|
3080
|
+
var InvoiceService = class {
|
|
3081
|
+
constructor(repository) {
|
|
3082
|
+
this.repository = repository;
|
|
3083
|
+
}
|
|
3084
|
+
/**
|
|
3085
|
+
* Issue a new invoice
|
|
3086
|
+
*/
|
|
3087
|
+
async issue(input) {
|
|
3088
|
+
const items = input.items.map((item) => {
|
|
3089
|
+
const unitPrice = Money.create(item.unitPrice);
|
|
3090
|
+
if (item.amount !== void 0) {
|
|
3091
|
+
return InvoiceItem.create({
|
|
3092
|
+
description: item.description,
|
|
3093
|
+
quantity: item.quantity,
|
|
3094
|
+
unitPrice,
|
|
3095
|
+
amount: Money.create(item.amount),
|
|
3096
|
+
unit: item.unit,
|
|
3097
|
+
remark: item.remark,
|
|
3098
|
+
taxType: item.taxType
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
return InvoiceItem.createWithAutoAmount({
|
|
3102
|
+
description: item.description,
|
|
3103
|
+
quantity: item.quantity,
|
|
3104
|
+
unitPrice,
|
|
3105
|
+
unit: item.unit,
|
|
3106
|
+
remark: item.remark,
|
|
3107
|
+
taxType: item.taxType
|
|
3108
|
+
});
|
|
3109
|
+
});
|
|
3110
|
+
const buyerTaxId = input.buyerTaxId ? TaxId.tryCreate(input.buyerTaxId) ?? TaxId.none() : TaxId.none();
|
|
3111
|
+
const buyer = buyerTaxId.isNone() ? Buyer.anonymous(input.buyerName) : Buyer.company({
|
|
3112
|
+
taxId: buyerTaxId,
|
|
3113
|
+
name: input.buyerName,
|
|
3114
|
+
address: input.buyerAddress,
|
|
3115
|
+
phone: input.buyerPhone,
|
|
3116
|
+
email: input.buyerEmail
|
|
3117
|
+
});
|
|
3118
|
+
let carrier;
|
|
3119
|
+
if (input.carrier) {
|
|
3120
|
+
switch (input.carrier.type) {
|
|
3121
|
+
case "mobile":
|
|
3122
|
+
carrier = Carrier.mobile(input.carrier.value);
|
|
3123
|
+
break;
|
|
3124
|
+
case "certificate":
|
|
3125
|
+
carrier = Carrier.certificate(input.carrier.value);
|
|
3126
|
+
break;
|
|
3127
|
+
case "amego":
|
|
3128
|
+
carrier = Carrier.custom("amego", input.carrier.value);
|
|
3129
|
+
break;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
const donation = input.donationCode ? Donation.tryCreate(input.donationCode) : void 0;
|
|
3133
|
+
const invoice = Invoice.create({
|
|
3134
|
+
orderId: OrderId.create(input.orderId),
|
|
3135
|
+
buyer,
|
|
3136
|
+
items,
|
|
3137
|
+
carrier,
|
|
3138
|
+
donation,
|
|
3139
|
+
remark: input.remark,
|
|
3140
|
+
trackApiCode: input.trackApiCode,
|
|
3141
|
+
customsClearanceMark: input.customsClearanceMark,
|
|
3142
|
+
zeroTaxRateReason: input.zeroTaxRateReason,
|
|
3143
|
+
brandName: input.brandName,
|
|
3144
|
+
pricesIncludeTax: input.pricesIncludeTax
|
|
3145
|
+
});
|
|
3146
|
+
return this.repository.issue(invoice);
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Void an invoice
|
|
3150
|
+
*/
|
|
3151
|
+
async void(invoiceNumber) {
|
|
3152
|
+
const number = InvoiceNumber.create(invoiceNumber);
|
|
3153
|
+
return this.repository.void(number);
|
|
3154
|
+
}
|
|
3155
|
+
/**
|
|
3156
|
+
* Query invoice by invoice number
|
|
3157
|
+
*/
|
|
3158
|
+
async findByInvoiceNumber(invoiceNumber) {
|
|
3159
|
+
const number = InvoiceNumber.create(invoiceNumber);
|
|
3160
|
+
return this.repository.findByInvoiceNumber(number);
|
|
3161
|
+
}
|
|
3162
|
+
/**
|
|
3163
|
+
* Query invoice by order ID
|
|
3164
|
+
*/
|
|
3165
|
+
async findByOrderId(orderId) {
|
|
3166
|
+
return this.repository.findByOrderId(orderId);
|
|
3167
|
+
}
|
|
3168
|
+
/**
|
|
3169
|
+
* Check invoice status
|
|
3170
|
+
*/
|
|
3171
|
+
async getStatus(invoiceNumbers) {
|
|
3172
|
+
const numbers = invoiceNumbers.map((n) => InvoiceNumber.create(n));
|
|
3173
|
+
return this.repository.getStatus(numbers);
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* List invoices
|
|
3177
|
+
*/
|
|
3178
|
+
async list(options) {
|
|
3179
|
+
return this.repository.list(options);
|
|
3180
|
+
}
|
|
3181
|
+
};
|
|
3182
|
+
|
|
3183
|
+
// src/application/AllowanceService.ts
|
|
3184
|
+
var AllowanceService = class {
|
|
3185
|
+
constructor(repository) {
|
|
3186
|
+
this.repository = repository;
|
|
3187
|
+
}
|
|
3188
|
+
/**
|
|
3189
|
+
* Issue a new allowance
|
|
3190
|
+
*/
|
|
3191
|
+
async issue(input) {
|
|
3192
|
+
const items = input.items.map((item) => {
|
|
3193
|
+
const unitPrice = Money.create(item.unitPrice);
|
|
3194
|
+
if (item.amount !== void 0) {
|
|
3195
|
+
return AllowanceItem.create({
|
|
3196
|
+
originalDescription: item.originalDescription,
|
|
3197
|
+
quantity: item.quantity,
|
|
3198
|
+
unitPrice,
|
|
3199
|
+
amount: Money.create(item.amount),
|
|
3200
|
+
unit: item.unit,
|
|
3201
|
+
taxType: item.taxType
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3204
|
+
return AllowanceItem.createWithAutoAmount({
|
|
3205
|
+
originalDescription: item.originalDescription,
|
|
3206
|
+
quantity: item.quantity,
|
|
3207
|
+
unitPrice,
|
|
3208
|
+
unit: item.unit,
|
|
3209
|
+
taxType: item.taxType
|
|
3210
|
+
});
|
|
3211
|
+
});
|
|
3212
|
+
let invoiceDate;
|
|
3213
|
+
if (typeof input.originalInvoiceDate === "string") {
|
|
3214
|
+
const dateStr = input.originalInvoiceDate;
|
|
3215
|
+
if (dateStr.length === 8) {
|
|
3216
|
+
const year = parseInt(dateStr.substring(0, 4), 10);
|
|
3217
|
+
const month = parseInt(dateStr.substring(4, 6), 10) - 1;
|
|
3218
|
+
const day = parseInt(dateStr.substring(6, 8), 10);
|
|
3219
|
+
invoiceDate = new Date(year, month, day);
|
|
3220
|
+
} else {
|
|
3221
|
+
invoiceDate = new Date(dateStr);
|
|
3222
|
+
}
|
|
3223
|
+
} else {
|
|
3224
|
+
invoiceDate = input.originalInvoiceDate;
|
|
3225
|
+
}
|
|
3226
|
+
const props = {
|
|
3227
|
+
originalInvoiceNumber: InvoiceNumber.create(input.originalInvoiceNumber),
|
|
3228
|
+
originalInvoiceDate: invoiceDate,
|
|
3229
|
+
buyerTaxId: input.buyerTaxId ? TaxId.tryCreate(input.buyerTaxId) ?? TaxId.none() : TaxId.none(),
|
|
3230
|
+
sellerTaxId: TaxId.create(input.sellerTaxId),
|
|
3231
|
+
items,
|
|
3232
|
+
buyerName: input.buyerName,
|
|
3233
|
+
sellerName: input.sellerName,
|
|
3234
|
+
pricesIncludeTax: input.pricesIncludeTax
|
|
3235
|
+
};
|
|
3236
|
+
const allowance = Allowance.create(props);
|
|
3237
|
+
return this.repository.issue(allowance);
|
|
3238
|
+
}
|
|
3239
|
+
/**
|
|
3240
|
+
* Void an allowance
|
|
3241
|
+
*/
|
|
3242
|
+
async void(allowanceNumber) {
|
|
3243
|
+
return this.repository.void(allowanceNumber);
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Query allowance by allowance number
|
|
3247
|
+
*/
|
|
3248
|
+
async findByAllowanceNumber(allowanceNumber) {
|
|
3249
|
+
return this.repository.findByAllowanceNumber(allowanceNumber);
|
|
3250
|
+
}
|
|
3251
|
+
/**
|
|
3252
|
+
* Query allowances by original invoice number
|
|
3253
|
+
*/
|
|
3254
|
+
async findByInvoiceNumber(invoiceNumber) {
|
|
3255
|
+
const number = InvoiceNumber.create(invoiceNumber);
|
|
3256
|
+
return this.repository.findByInvoiceNumber(number);
|
|
3257
|
+
}
|
|
3258
|
+
/**
|
|
3259
|
+
* Check allowance status
|
|
3260
|
+
*/
|
|
3261
|
+
async getStatus(allowanceNumbers) {
|
|
3262
|
+
return this.repository.getStatus(allowanceNumbers);
|
|
3263
|
+
}
|
|
3264
|
+
/**
|
|
3265
|
+
* List allowances
|
|
3266
|
+
*/
|
|
3267
|
+
async list(options) {
|
|
3268
|
+
return this.repository.list(options);
|
|
3269
|
+
}
|
|
3270
|
+
};
|
|
3271
|
+
|
|
3272
|
+
export { AMEGO_DEFAULTS, AMEGO_ENDPOINTS, Allowance, AllowanceItem, AllowanceStatus, AllowanceType, AmegoAllowanceRepository, AmegoAllowanceService, AmegoClient, AmegoInvoiceRepository, AmegoInvoiceService, AmegoMapper, AmegoSigner, Buyer, Capability, Carrier, CarrierCode, CarrierType2 as CarrierType, CustomsClearanceMark, DateRange, DateType, Donation, InvalidCarrierCodeError, InvalidInvoiceNumberError, InvalidMoneyError, InvalidTaxIdError, Invoice, InvoiceItem, InvoiceNumber, InvoiceQuery, InvoiceStatus, InvoiceType, AllowanceService as LegacyAllowanceService, InvoiceService as LegacyInvoiceService, LocalDate, Money, NetworkError, OrderId, PROVIDER_CAPABILITIES, PROVIDER_NAMES, Pagination, Provider, ProviderApiError, ProviderNotImplementedError, TAX_RATE, TaxCalculator, TaxId, TaxType, UnsupportedCapabilityError, ValidationError, ZeroTaxRateReason, Zinvoice, ZinvoiceError, createPaginatedResult };
|
|
3273
|
+
//# sourceMappingURL=index.js.map
|
|
3274
|
+
//# sourceMappingURL=index.js.map
|