@gravito/satellite-invoice 0.1.4 → 0.2.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/CHANGELOG.md +8 -0
- package/REFACTOR_PLAN.md +238 -0
- package/dist/index.js +1097 -56
- package/package.json +4 -2
- package/package.json.bak +29 -0
- package/src/Application/Contexts/InvoiceAuditContext.ts +96 -0
- package/src/Application/Contexts/InvoiceCancellationContext.ts +60 -0
- package/src/Application/Contexts/InvoiceIssuanceContext.ts +42 -0
- package/src/Application/Roles/InvoiceCancellerRole.ts +97 -0
- package/src/Application/Roles/InvoiceIssuerRole.ts +72 -0
- package/src/Application/Roles/InvoiceTrackerRole.ts +112 -0
- package/src/Application/UseCases/CancelInvoice.ts +32 -0
- package/src/Application/UseCases/IssueInvoice.ts +13 -26
- package/src/Application/UseCases/QueryInvoiceStatus.ts +56 -0
- package/src/Domain/Contracts/IInvoiceRepository.ts +3 -0
- package/src/Domain/Entities/Invoice.ts +178 -20
- package/src/Domain/Errors/InvoiceError.ts +89 -0
- package/src/Domain/ValueObjects/InvoiceAmount.ts +86 -0
- package/src/Domain/ValueObjects/InvoiceNumber.ts +52 -0
- package/src/Domain/ValueObjects/InvoiceStatus.ts +131 -0
- package/src/Domain/ValueObjects/InvoiceTax.ts +71 -0
- package/src/Infrastructure/Persistence/AtlasInvoiceRepository.ts +135 -6
- package/src/Interface/Http/Controllers/AdminInvoiceController.ts +264 -18
- package/src/index.ts +57 -3
- package/tests/Application/Contexts/InvoiceAuditContext.test.ts +198 -0
- package/tests/Application/Contexts/InvoiceCancellationContext.test.ts +232 -0
- package/tests/Application/Contexts/InvoiceIssuanceContext.test.ts +180 -0
- package/tests/Application/Roles/InvoiceCancellerRole.test.ts +109 -0
- package/tests/Application/Roles/InvoiceIssuerRole.test.ts +66 -0
- package/tests/Application/Roles/InvoiceTrackerRole.test.ts +126 -0
- package/tests/Application/UseCases/CancelInvoice.test.ts +175 -0
- package/tests/Application/UseCases/IssueInvoice.test.ts +169 -0
- package/tests/Application/UseCases/QueryInvoiceStatus.test.ts +191 -0
- package/tests/Domain/Errors/InvoiceError.test.ts +96 -0
- package/tests/Domain/ValueObjects/InvoiceAmount.test.ts +84 -0
- package/tests/Domain/ValueObjects/InvoiceNumber.test.ts +60 -0
- package/tests/Domain/ValueObjects/InvoiceStatus.test.ts +89 -0
- package/tests/Domain/ValueObjects/InvoiceTax.test.ts +66 -0
- package/tests/Interface/Http/Controllers/AdminInvoiceController.test.ts +294 -0
- package/tests/domain.test.ts +244 -0
- package/tsconfig.json +11 -19
- package/dist/index.d.ts +0 -8
|
@@ -6,11 +6,25 @@ class InvoiceModel extends Model {
|
|
|
6
6
|
static override table = 'invoices'
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Atlas ORM 發票 Repository
|
|
11
|
+
* 實現 IInvoiceRepository 介面
|
|
12
|
+
*/
|
|
9
13
|
export class AtlasInvoiceRepository implements IInvoiceRepository {
|
|
10
14
|
async save(invoice: Invoice): Promise<void> {
|
|
15
|
+
const snapshot = invoice.toSnapshot()
|
|
11
16
|
const data = {
|
|
12
|
-
id:
|
|
13
|
-
|
|
17
|
+
id: snapshot.id,
|
|
18
|
+
orderId: snapshot.orderId,
|
|
19
|
+
invoiceNumber: snapshot.invoiceNumber,
|
|
20
|
+
amount: snapshot.amount,
|
|
21
|
+
amountCurrency: snapshot.amountCurrency,
|
|
22
|
+
tax: snapshot.tax,
|
|
23
|
+
taxRate: snapshot.taxRate,
|
|
24
|
+
status: snapshot.status,
|
|
25
|
+
buyerIdentifier: snapshot.buyerIdentifier,
|
|
26
|
+
carrierId: snapshot.carrierId,
|
|
27
|
+
createdAt: snapshot.createdAt,
|
|
14
28
|
}
|
|
15
29
|
|
|
16
30
|
const existing = await InvoiceModel.find(invoice.id)
|
|
@@ -23,16 +37,131 @@ export class AtlasInvoiceRepository implements IInvoiceRepository {
|
|
|
23
37
|
|
|
24
38
|
async findById(id: string): Promise<Invoice | null> {
|
|
25
39
|
const row = await InvoiceModel.find(id)
|
|
26
|
-
|
|
40
|
+
if (!row) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
const snapshot = row as any
|
|
44
|
+
return Invoice.fromSnapshot({
|
|
45
|
+
id: snapshot.id,
|
|
46
|
+
orderId: snapshot.orderId,
|
|
47
|
+
invoiceNumber: snapshot.invoiceNumber,
|
|
48
|
+
amount: snapshot.amount,
|
|
49
|
+
amountCurrency: snapshot.amountCurrency,
|
|
50
|
+
tax: snapshot.tax,
|
|
51
|
+
taxRate: snapshot.taxRate,
|
|
52
|
+
status: snapshot.status,
|
|
53
|
+
buyerIdentifier: snapshot.buyerIdentifier,
|
|
54
|
+
carrierId: snapshot.carrierId,
|
|
55
|
+
createdAt: snapshot.createdAt,
|
|
56
|
+
})
|
|
27
57
|
}
|
|
28
58
|
|
|
29
59
|
async findByOrderId(orderId: string): Promise<Invoice | null> {
|
|
30
|
-
const row = await (InvoiceModel.query() as any).where('
|
|
31
|
-
|
|
60
|
+
const row = await (InvoiceModel.query() as any).where('orderId', orderId).first()
|
|
61
|
+
if (!row) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
const snapshot = row
|
|
65
|
+
return Invoice.fromSnapshot({
|
|
66
|
+
id: snapshot.id,
|
|
67
|
+
orderId: snapshot.orderId,
|
|
68
|
+
invoiceNumber: snapshot.invoiceNumber,
|
|
69
|
+
amount: snapshot.amount,
|
|
70
|
+
amountCurrency: snapshot.amountCurrency,
|
|
71
|
+
tax: snapshot.tax,
|
|
72
|
+
taxRate: snapshot.taxRate,
|
|
73
|
+
status: snapshot.status,
|
|
74
|
+
buyerIdentifier: snapshot.buyerIdentifier,
|
|
75
|
+
carrierId: snapshot.carrierId,
|
|
76
|
+
createdAt: snapshot.createdAt,
|
|
77
|
+
})
|
|
32
78
|
}
|
|
33
79
|
|
|
34
80
|
async findAll(): Promise<Invoice[]> {
|
|
35
81
|
const rows = await InvoiceModel.all()
|
|
36
|
-
return rows.map((row) =>
|
|
82
|
+
return rows.map((row) =>
|
|
83
|
+
Invoice.fromSnapshot({
|
|
84
|
+
id: (row as any).id,
|
|
85
|
+
orderId: (row as any).orderId,
|
|
86
|
+
invoiceNumber: (row as any).invoiceNumber,
|
|
87
|
+
amount: (row as any).amount,
|
|
88
|
+
amountCurrency: (row as any).amountCurrency,
|
|
89
|
+
tax: (row as any).tax,
|
|
90
|
+
taxRate: (row as any).taxRate,
|
|
91
|
+
status: (row as any).status,
|
|
92
|
+
buyerIdentifier: (row as any).buyerIdentifier,
|
|
93
|
+
carrierId: (row as any).carrierId,
|
|
94
|
+
createdAt: (row as any).createdAt,
|
|
95
|
+
})
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 按發票號碼查詢
|
|
101
|
+
*/
|
|
102
|
+
async findByInvoiceNumber(invoiceNumber: string): Promise<Invoice | null> {
|
|
103
|
+
const row = await (InvoiceModel.query() as any).where('invoiceNumber', invoiceNumber).first()
|
|
104
|
+
if (!row) {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
const snapshot = row
|
|
108
|
+
return Invoice.fromSnapshot({
|
|
109
|
+
id: snapshot.id,
|
|
110
|
+
orderId: snapshot.orderId,
|
|
111
|
+
invoiceNumber: snapshot.invoiceNumber,
|
|
112
|
+
amount: snapshot.amount,
|
|
113
|
+
amountCurrency: snapshot.amountCurrency,
|
|
114
|
+
tax: snapshot.tax,
|
|
115
|
+
taxRate: snapshot.taxRate,
|
|
116
|
+
status: snapshot.status,
|
|
117
|
+
buyerIdentifier: snapshot.buyerIdentifier,
|
|
118
|
+
carrierId: snapshot.carrierId,
|
|
119
|
+
createdAt: snapshot.createdAt,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 按狀態查詢
|
|
125
|
+
*/
|
|
126
|
+
async findByStatus(status: string): Promise<Invoice[]> {
|
|
127
|
+
const rows = await (InvoiceModel.query() as any).where('status', status)
|
|
128
|
+
return rows.map((row: any) =>
|
|
129
|
+
Invoice.fromSnapshot({
|
|
130
|
+
id: row.id,
|
|
131
|
+
orderId: row.orderId,
|
|
132
|
+
invoiceNumber: row.invoiceNumber,
|
|
133
|
+
amount: row.amount,
|
|
134
|
+
amountCurrency: row.amountCurrency,
|
|
135
|
+
tax: row.tax,
|
|
136
|
+
taxRate: row.taxRate,
|
|
137
|
+
status: row.status,
|
|
138
|
+
buyerIdentifier: row.buyerIdentifier,
|
|
139
|
+
carrierId: row.carrierId,
|
|
140
|
+
createdAt: row.createdAt,
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 按日期範圍查詢
|
|
147
|
+
*/
|
|
148
|
+
async findByDateRange(startDate: Date, endDate: Date): Promise<Invoice[]> {
|
|
149
|
+
const rows = await (InvoiceModel.query() as any).whereBetween('createdAt', [startDate, endDate])
|
|
150
|
+
|
|
151
|
+
return rows.map((row: any) =>
|
|
152
|
+
Invoice.fromSnapshot({
|
|
153
|
+
id: row.id,
|
|
154
|
+
orderId: row.orderId,
|
|
155
|
+
invoiceNumber: row.invoiceNumber,
|
|
156
|
+
amount: row.amount,
|
|
157
|
+
amountCurrency: row.amountCurrency,
|
|
158
|
+
tax: row.tax,
|
|
159
|
+
taxRate: row.taxRate,
|
|
160
|
+
status: row.status,
|
|
161
|
+
buyerIdentifier: row.buyerIdentifier,
|
|
162
|
+
carrierId: row.carrierId,
|
|
163
|
+
createdAt: row.createdAt,
|
|
164
|
+
})
|
|
165
|
+
)
|
|
37
166
|
}
|
|
38
167
|
}
|
|
@@ -1,32 +1,278 @@
|
|
|
1
1
|
import type { PlanetCore } from '@gravito/core'
|
|
2
|
+
import type { CancelInvoice } from '../../../Application/UseCases/CancelInvoice'
|
|
2
3
|
import type { IssueInvoice } from '../../../Application/UseCases/IssueInvoice'
|
|
4
|
+
import type {
|
|
5
|
+
GenerateInvoiceReport,
|
|
6
|
+
QueryInvoiceStatus,
|
|
7
|
+
} from '../../../Application/UseCases/QueryInvoiceStatus'
|
|
3
8
|
import type { Invoice } from '../../../Domain/Entities/Invoice'
|
|
9
|
+
import {
|
|
10
|
+
DuplicateInvoiceError,
|
|
11
|
+
InvalidCancellationError,
|
|
12
|
+
InvoiceError,
|
|
13
|
+
InvoiceNotFoundError,
|
|
14
|
+
} from '../../../Domain/Errors/InvoiceError'
|
|
4
15
|
|
|
16
|
+
/**
|
|
17
|
+
* 發票管理控制器
|
|
18
|
+
* 提供完整的 API 端點
|
|
19
|
+
*/
|
|
5
20
|
export class AdminInvoiceController {
|
|
6
21
|
constructor(private core: PlanetCore) {}
|
|
7
22
|
|
|
23
|
+
/**
|
|
24
|
+
* GET /invoices - 列表查詢發票
|
|
25
|
+
*/
|
|
8
26
|
async index(ctx: any) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
try {
|
|
28
|
+
const repo = this.core.container.make('invoice.repository') as any
|
|
29
|
+
const invoices = await repo.findAll()
|
|
30
|
+
|
|
31
|
+
return ctx.json({
|
|
32
|
+
success: true,
|
|
33
|
+
data: invoices.map((inv: Invoice) => inv.unpack()),
|
|
34
|
+
meta: {
|
|
35
|
+
total: invoices.length,
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
} catch (error) {
|
|
39
|
+
this.logError('Failed to list invoices', error)
|
|
40
|
+
return ctx.json(
|
|
41
|
+
{
|
|
42
|
+
success: false,
|
|
43
|
+
error: '無法獲取發票列表',
|
|
44
|
+
},
|
|
45
|
+
500
|
|
46
|
+
)
|
|
47
|
+
}
|
|
18
48
|
}
|
|
19
49
|
|
|
50
|
+
/**
|
|
51
|
+
* POST /invoices - 開立新發票
|
|
52
|
+
*/
|
|
20
53
|
async store(ctx: any) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
54
|
+
try {
|
|
55
|
+
const body = await ctx.req.json()
|
|
56
|
+
|
|
57
|
+
// 驗證必要欄位
|
|
58
|
+
if (!body.orderId || !body.amount) {
|
|
59
|
+
return ctx.json(
|
|
60
|
+
{
|
|
61
|
+
success: false,
|
|
62
|
+
error: '缺少必要欄位:orderId, amount',
|
|
63
|
+
},
|
|
64
|
+
400
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const useCase = this.core.container.make<IssueInvoice>('invoice.usecase.issue')
|
|
69
|
+
const invoice = await useCase.execute(body)
|
|
70
|
+
|
|
71
|
+
return ctx.json({
|
|
72
|
+
success: true,
|
|
73
|
+
data: {
|
|
74
|
+
id: invoice.id,
|
|
75
|
+
invoiceNumber: invoice.invoiceNumber,
|
|
76
|
+
orderId: invoice.orderId,
|
|
77
|
+
amount: invoice.amount,
|
|
78
|
+
status: invoice.status,
|
|
79
|
+
createdAt: invoice.createdAt,
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return this.handleError(ctx, error, 'Failed to issue invoice')
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* GET /invoices/:id - 查詢發票狀態
|
|
89
|
+
*/
|
|
90
|
+
async show(ctx: any) {
|
|
91
|
+
try {
|
|
92
|
+
const invoiceId = ctx.param('id')
|
|
93
|
+
|
|
94
|
+
if (!invoiceId) {
|
|
95
|
+
return ctx.json(
|
|
96
|
+
{
|
|
97
|
+
success: false,
|
|
98
|
+
error: '缺少發票 ID',
|
|
99
|
+
},
|
|
100
|
+
400
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const useCase = this.core.container.make<QueryInvoiceStatus>('invoice.usecase.query')
|
|
105
|
+
const result = await useCase.execute({ invoiceId })
|
|
106
|
+
|
|
107
|
+
return ctx.json({
|
|
108
|
+
success: true,
|
|
109
|
+
data: result,
|
|
110
|
+
})
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return this.handleError(ctx, error, 'Failed to query invoice status')
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* POST /invoices/:id/cancel - 取消發票
|
|
118
|
+
*/
|
|
119
|
+
async cancel(ctx: any) {
|
|
120
|
+
try {
|
|
121
|
+
const invoiceId = ctx.param('id')
|
|
122
|
+
const body = await ctx.req.json()
|
|
123
|
+
|
|
124
|
+
if (!invoiceId) {
|
|
125
|
+
return ctx.json(
|
|
126
|
+
{
|
|
127
|
+
success: false,
|
|
128
|
+
error: '缺少發票 ID',
|
|
129
|
+
},
|
|
130
|
+
400
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!body.reason) {
|
|
135
|
+
return ctx.json(
|
|
136
|
+
{
|
|
137
|
+
success: false,
|
|
138
|
+
error: '缺少取消原因',
|
|
139
|
+
},
|
|
140
|
+
400
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const useCase = this.core.container.make<CancelInvoice>('invoice.usecase.cancel')
|
|
145
|
+
const result = await useCase.execute({
|
|
146
|
+
invoiceId,
|
|
147
|
+
reason: body.reason,
|
|
148
|
+
notes: body.notes,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
return ctx.json({
|
|
152
|
+
success: true,
|
|
153
|
+
data: result,
|
|
154
|
+
})
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return this.handleError(ctx, error, 'Failed to cancel invoice')
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* GET /invoices/report - 生成發票報告
|
|
162
|
+
*/
|
|
163
|
+
async report(ctx: any) {
|
|
164
|
+
try {
|
|
165
|
+
const startDate = ctx.query('startDate')
|
|
166
|
+
const endDate = ctx.query('endDate')
|
|
167
|
+
|
|
168
|
+
if (!startDate || !endDate) {
|
|
169
|
+
return ctx.json(
|
|
170
|
+
{
|
|
171
|
+
success: false,
|
|
172
|
+
error: '缺少日期範圍參數:startDate, endDate',
|
|
173
|
+
},
|
|
174
|
+
400
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const start = new Date(startDate)
|
|
179
|
+
const end = new Date(endDate)
|
|
180
|
+
|
|
181
|
+
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
|
182
|
+
return ctx.json(
|
|
183
|
+
{
|
|
184
|
+
success: false,
|
|
185
|
+
error: '無效的日期格式',
|
|
186
|
+
},
|
|
187
|
+
400
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const useCase = this.core.container.make<GenerateInvoiceReport>('invoice.usecase.report')
|
|
192
|
+
const report = await useCase.execute({
|
|
193
|
+
startDate: start,
|
|
194
|
+
endDate: end,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
return ctx.json({
|
|
198
|
+
success: true,
|
|
199
|
+
data: report,
|
|
200
|
+
})
|
|
201
|
+
} catch (error) {
|
|
202
|
+
this.logError('Failed to generate report', error)
|
|
203
|
+
return ctx.json(
|
|
204
|
+
{
|
|
205
|
+
success: false,
|
|
206
|
+
error: '無法生成報告',
|
|
207
|
+
},
|
|
208
|
+
500
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 處理錯誤
|
|
215
|
+
*/
|
|
216
|
+
private handleError(ctx: any, error: unknown, message: string): any {
|
|
217
|
+
if (error instanceof InvoiceNotFoundError) {
|
|
218
|
+
return ctx.json(
|
|
219
|
+
{
|
|
220
|
+
success: false,
|
|
221
|
+
error: error.message,
|
|
222
|
+
},
|
|
223
|
+
404
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (error instanceof DuplicateInvoiceError) {
|
|
228
|
+
return ctx.json(
|
|
229
|
+
{
|
|
230
|
+
success: false,
|
|
231
|
+
error: error.message,
|
|
232
|
+
},
|
|
233
|
+
409
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (error instanceof InvalidCancellationError) {
|
|
238
|
+
return ctx.json(
|
|
239
|
+
{
|
|
240
|
+
success: false,
|
|
241
|
+
error: error.message,
|
|
242
|
+
},
|
|
243
|
+
400
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (error instanceof InvoiceError) {
|
|
248
|
+
this.logError(message, error)
|
|
249
|
+
return ctx.json(
|
|
250
|
+
{
|
|
251
|
+
success: false,
|
|
252
|
+
error: error.message,
|
|
253
|
+
},
|
|
254
|
+
400
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.logError(message, error)
|
|
259
|
+
return ctx.json(
|
|
260
|
+
{
|
|
261
|
+
success: false,
|
|
262
|
+
error: '伺服器內部錯誤',
|
|
29
263
|
},
|
|
30
|
-
|
|
264
|
+
500
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 記錄錯誤
|
|
270
|
+
*/
|
|
271
|
+
private logError(message: string, error: unknown): void {
|
|
272
|
+
if (this.core?.logger) {
|
|
273
|
+
this.core.logger.error(
|
|
274
|
+
`[Invoice] ${message}: ${error instanceof Error ? error.message : String(error)}`
|
|
275
|
+
)
|
|
276
|
+
}
|
|
31
277
|
}
|
|
32
278
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,57 @@
|
|
|
1
1
|
import { type Container, ServiceProvider } from '@gravito/core'
|
|
2
|
+
import { CancelInvoice } from './Application/UseCases/CancelInvoice'
|
|
2
3
|
import { IssueInvoice } from './Application/UseCases/IssueInvoice'
|
|
4
|
+
import {
|
|
5
|
+
GenerateInvoiceReport,
|
|
6
|
+
QueryInvoiceStatus,
|
|
7
|
+
} from './Application/UseCases/QueryInvoiceStatus'
|
|
3
8
|
import { AtlasInvoiceRepository } from './Infrastructure/Persistence/AtlasInvoiceRepository'
|
|
4
9
|
import { AdminInvoiceController } from './Interface/Http/Controllers/AdminInvoiceController'
|
|
5
10
|
|
|
11
|
+
// Infrastructure Exports
|
|
12
|
+
export type { IInvoiceRepository } from './Domain/Contracts/IInvoiceRepository'
|
|
13
|
+
// Domain Exports
|
|
14
|
+
export { Invoice, type InvoiceProps, type InvoiceSnapshot } from './Domain/Entities/Invoice'
|
|
15
|
+
export {
|
|
16
|
+
DuplicateInvoiceError,
|
|
17
|
+
InvalidAmountError,
|
|
18
|
+
InvalidCancellationError,
|
|
19
|
+
InvalidInvoiceNumberError,
|
|
20
|
+
InvalidTaxError,
|
|
21
|
+
InvalidTransitionError,
|
|
22
|
+
InvoiceError,
|
|
23
|
+
InvoiceNotFoundError,
|
|
24
|
+
} from './Domain/Errors/InvoiceError'
|
|
25
|
+
export { InvoiceAmount, type InvoiceAmountProps } from './Domain/ValueObjects/InvoiceAmount'
|
|
26
|
+
export { InvoiceNumber, type InvoiceNumberProps } from './Domain/ValueObjects/InvoiceNumber'
|
|
27
|
+
export { InvoiceStatus, type InvoiceStatusType } from './Domain/ValueObjects/InvoiceStatus'
|
|
28
|
+
export { InvoiceTax, type InvoiceTaxProps } from './Domain/ValueObjects/InvoiceTax'
|
|
29
|
+
export { AtlasInvoiceRepository } from './Infrastructure/Persistence/AtlasInvoiceRepository'
|
|
30
|
+
|
|
6
31
|
export class InvoiceServiceProvider extends ServiceProvider {
|
|
7
32
|
register(container: Container): void {
|
|
33
|
+
// 註冊 Repository
|
|
8
34
|
container.singleton('invoice.repository', () => new AtlasInvoiceRepository())
|
|
35
|
+
|
|
36
|
+
// 註冊 UseCase
|
|
9
37
|
container.bind(
|
|
10
38
|
'invoice.usecase.issue',
|
|
11
39
|
() => new IssueInvoice(container.make('invoice.repository'))
|
|
12
40
|
)
|
|
41
|
+
container.bind(
|
|
42
|
+
'invoice.usecase.cancel',
|
|
43
|
+
() => new CancelInvoice(container.make('invoice.repository'))
|
|
44
|
+
)
|
|
45
|
+
container.bind(
|
|
46
|
+
'invoice.usecase.query',
|
|
47
|
+
() => new QueryInvoiceStatus(container.make('invoice.repository'))
|
|
48
|
+
)
|
|
49
|
+
container.bind(
|
|
50
|
+
'invoice.usecase.report',
|
|
51
|
+
() => new GenerateInvoiceReport(container.make('invoice.repository'))
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
// 註冊 Controller
|
|
13
55
|
container.singleton('invoice.controller.admin', () => new AdminInvoiceController(this.core!))
|
|
14
56
|
}
|
|
15
57
|
|
|
@@ -25,8 +67,20 @@ export class InvoiceServiceProvider extends ServiceProvider {
|
|
|
25
67
|
|
|
26
68
|
// 註冊管理路由
|
|
27
69
|
core.router.prefix('/api/admin/v1/invoices').group((router) => {
|
|
70
|
+
// 發票列表
|
|
28
71
|
router.get('/', (ctx) => controller.index(ctx))
|
|
72
|
+
|
|
73
|
+
// 開立發票
|
|
29
74
|
router.post('/', (ctx) => controller.store(ctx))
|
|
75
|
+
|
|
76
|
+
// 查詢發票狀態
|
|
77
|
+
router.get('/:id', (ctx) => controller.show(ctx))
|
|
78
|
+
|
|
79
|
+
// 取消發票
|
|
80
|
+
router.post('/:id/cancel', (ctx) => controller.cancel(ctx))
|
|
81
|
+
|
|
82
|
+
// 生成報告
|
|
83
|
+
router.get('/report', (ctx) => controller.report(ctx))
|
|
30
84
|
})
|
|
31
85
|
|
|
32
86
|
/**
|
|
@@ -40,13 +94,13 @@ export class InvoiceServiceProvider extends ServiceProvider {
|
|
|
40
94
|
const issueUseCase = core.container.make<IssueInvoice>('invoice.usecase.issue')
|
|
41
95
|
|
|
42
96
|
try {
|
|
43
|
-
const invoice = await issueUseCase.execute({
|
|
97
|
+
const invoice = (await issueUseCase.execute({
|
|
44
98
|
orderId: payload.orderId,
|
|
45
99
|
amount: payload.amount,
|
|
46
100
|
buyerIdentifier: payload.buyer?.identifier,
|
|
47
101
|
carrierId: payload.buyer?.carrierId,
|
|
48
|
-
})
|
|
49
|
-
core.logger.info(`[Invoice] Automatically issued: ${invoice.invoiceNumber}`)
|
|
102
|
+
})) as any
|
|
103
|
+
core.logger.info(`[Invoice] Automatically issued: ${invoice.invoiceNumber.value}`)
|
|
50
104
|
} catch (error: any) {
|
|
51
105
|
core.logger.error(`[Invoice] Auto-issue failed: ${error.message}`)
|
|
52
106
|
}
|