@gravito/satellite-invoice 0.1.5 → 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.
Files changed (40) hide show
  1. package/REFACTOR_PLAN.md +238 -0
  2. package/dist/index.js +1097 -56
  3. package/package.json +4 -2
  4. package/package.json.bak +29 -0
  5. package/src/Application/Contexts/InvoiceAuditContext.ts +96 -0
  6. package/src/Application/Contexts/InvoiceCancellationContext.ts +60 -0
  7. package/src/Application/Contexts/InvoiceIssuanceContext.ts +42 -0
  8. package/src/Application/Roles/InvoiceCancellerRole.ts +97 -0
  9. package/src/Application/Roles/InvoiceIssuerRole.ts +72 -0
  10. package/src/Application/Roles/InvoiceTrackerRole.ts +112 -0
  11. package/src/Application/UseCases/CancelInvoice.ts +32 -0
  12. package/src/Application/UseCases/IssueInvoice.ts +13 -26
  13. package/src/Application/UseCases/QueryInvoiceStatus.ts +56 -0
  14. package/src/Domain/Contracts/IInvoiceRepository.ts +3 -0
  15. package/src/Domain/Entities/Invoice.ts +178 -20
  16. package/src/Domain/Errors/InvoiceError.ts +89 -0
  17. package/src/Domain/ValueObjects/InvoiceAmount.ts +86 -0
  18. package/src/Domain/ValueObjects/InvoiceNumber.ts +52 -0
  19. package/src/Domain/ValueObjects/InvoiceStatus.ts +131 -0
  20. package/src/Domain/ValueObjects/InvoiceTax.ts +71 -0
  21. package/src/Infrastructure/Persistence/AtlasInvoiceRepository.ts +135 -6
  22. package/src/Interface/Http/Controllers/AdminInvoiceController.ts +264 -18
  23. package/src/index.ts +57 -3
  24. package/tests/Application/Contexts/InvoiceAuditContext.test.ts +198 -0
  25. package/tests/Application/Contexts/InvoiceCancellationContext.test.ts +232 -0
  26. package/tests/Application/Contexts/InvoiceIssuanceContext.test.ts +180 -0
  27. package/tests/Application/Roles/InvoiceCancellerRole.test.ts +109 -0
  28. package/tests/Application/Roles/InvoiceIssuerRole.test.ts +66 -0
  29. package/tests/Application/Roles/InvoiceTrackerRole.test.ts +126 -0
  30. package/tests/Application/UseCases/CancelInvoice.test.ts +175 -0
  31. package/tests/Application/UseCases/IssueInvoice.test.ts +169 -0
  32. package/tests/Application/UseCases/QueryInvoiceStatus.test.ts +191 -0
  33. package/tests/Domain/Errors/InvoiceError.test.ts +96 -0
  34. package/tests/Domain/ValueObjects/InvoiceAmount.test.ts +84 -0
  35. package/tests/Domain/ValueObjects/InvoiceNumber.test.ts +60 -0
  36. package/tests/Domain/ValueObjects/InvoiceStatus.test.ts +89 -0
  37. package/tests/Domain/ValueObjects/InvoiceTax.test.ts +66 -0
  38. package/tests/Interface/Http/Controllers/AdminInvoiceController.test.ts +294 -0
  39. package/tests/domain.test.ts +244 -0
  40. package/dist/index.d.ts +0 -8
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@gravito/satellite-invoice",
3
- "version": "0.1.5",
3
+ "sideEffects": false,
4
+ "version": "0.2.0",
4
5
  "type": "module",
5
6
  "main": "dist/index.js",
6
7
  "types": "dist/index.d.ts",
7
8
  "scripts": {
8
- "build": "tsup src/index.ts --format esm --dts --clean --external @gravito/enterprise --external @gravito/atlas --external @gravito/core",
9
+ "build": "tsup src/index.ts --format esm --clean --external @gravito/enterprise --external @gravito/atlas --external @gravito/core",
10
+ "build:dts": "tsup src/index.ts --format esm --dts --clean --external @gravito/enterprise --external @gravito/atlas --external @gravito/core",
9
11
  "typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck"
10
12
  },
11
13
  "dependencies": {
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@gravito/satellite-invoice",
3
+ "version": "0.1.4",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsup src/index.ts --format esm --dts --clean --external @gravito/enterprise --external @gravito/atlas --external @gravito/core",
9
+ "typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck"
10
+ },
11
+ "dependencies": {
12
+ "@gravito/enterprise": "workspace:*",
13
+ "@gravito/atlas": "workspace:*",
14
+ "@gravito/core": "workspace:*"
15
+ },
16
+ "devDependencies": {
17
+ "tsup": "^8.3.5",
18
+ "typescript": "^5.9.3"
19
+ },
20
+ "module": "dist/index.js",
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/gravito-framework/gravito.git",
27
+ "directory": "satellites/invoice"
28
+ }
29
+ }
@@ -0,0 +1,96 @@
1
+ import type { IInvoiceRepository } from '../../Domain/Contracts/IInvoiceRepository'
2
+ import { InvoiceNotFoundError } from '../../Domain/Errors/InvoiceError'
3
+ import type { InvoiceTrackerRole } from '../Roles/InvoiceTrackerRole'
4
+
5
+ export interface QueryStatusInput {
6
+ invoiceId: string
7
+ }
8
+
9
+ export interface ReportInput {
10
+ startDate: Date
11
+ endDate: Date
12
+ }
13
+
14
+ export interface AuditTrailOutput {
15
+ invoiceId: string
16
+ logs: Array<{
17
+ action: string
18
+ timestamp: Date
19
+ details?: Record<string, any>
20
+ }>
21
+ }
22
+
23
+ /**
24
+ * 發票審計上下文 (DCI)
25
+ * 編排發票查詢和報告的流程
26
+ */
27
+ export class InvoiceAuditContext {
28
+ constructor(
29
+ private repository: IInvoiceRepository,
30
+ private tracker: InvoiceTrackerRole
31
+ ) {}
32
+
33
+ async queryStatus(input: QueryStatusInput) {
34
+ const invoice = await this.repository.findById(input.invoiceId)
35
+ if (!invoice) {
36
+ throw new InvoiceNotFoundError(input.invoiceId)
37
+ }
38
+
39
+ await this.tracker.trackStatus(invoice)
40
+
41
+ return {
42
+ id: invoice.id,
43
+ invoiceNumber: invoice.invoiceNumberObject.value,
44
+ orderId: invoice.orderId,
45
+ status: invoice.statusObject.value,
46
+ amount: invoice.amountObject.value,
47
+ createdAt: invoice.createdAt,
48
+ }
49
+ }
50
+
51
+ async generateReport(input: ReportInput) {
52
+ const invoices = await this.repository.findByDateRange(input.startDate, input.endDate)
53
+
54
+ // 追蹤這些發票以供審計使用
55
+ for (const invoice of invoices) {
56
+ await this.tracker.trackStatus(invoice)
57
+ }
58
+
59
+ return {
60
+ period: { startDate: input.startDate, endDate: input.endDate },
61
+ summary: {
62
+ total: invoices.length,
63
+ issued: invoices.filter((inv) => inv.statusObject.isIssued()).length,
64
+ cancelled: invoices.filter((inv) => inv.statusObject.isCancelled()).length,
65
+ returned: invoices.filter((inv) => inv.statusObject.isReturned()).length,
66
+ totalAmount: invoices.reduce((sum: number, inv) => sum + inv.amountObject.value, 0),
67
+ },
68
+ invoices: invoices.map((inv) => ({
69
+ id: inv.id,
70
+ number: inv.invoiceNumberObject.value,
71
+ orderId: inv.orderId,
72
+ amount: inv.amountObject.value,
73
+ status: inv.statusObject.value,
74
+ createdAt: inv.createdAt,
75
+ })),
76
+ }
77
+ }
78
+
79
+ async getAuditTrail(input: { invoiceId: string }): Promise<AuditTrailOutput> {
80
+ const invoice = await this.repository.findById(input.invoiceId)
81
+ if (!invoice) {
82
+ throw new InvoiceNotFoundError(input.invoiceId)
83
+ }
84
+
85
+ const logs = await this.tracker.auditTrail(input.invoiceId)
86
+
87
+ return {
88
+ invoiceId: input.invoiceId,
89
+ logs: logs.map((log) => ({
90
+ action: log.action,
91
+ timestamp: log.timestamp,
92
+ details: log.details,
93
+ })),
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,60 @@
1
+ import type { IInvoiceRepository } from '../../Domain/Contracts/IInvoiceRepository'
2
+ import { InvalidCancellationError, InvoiceNotFoundError } from '../../Domain/Errors/InvoiceError'
3
+ import type { CancellationReason, InvoiceCancellerRole } from '../Roles/InvoiceCancellerRole'
4
+
5
+ export interface CancelInvoiceInput {
6
+ invoiceId: string
7
+ reason: CancellationReason
8
+ notes?: string
9
+ }
10
+
11
+ export interface CancelInvoiceOutput {
12
+ id: string
13
+ invoiceNumber: string
14
+ previousStatus: string
15
+ newStatus: string
16
+ cancelledAt: Date
17
+ }
18
+
19
+ /**
20
+ * 發票取消上下文 (DCI)
21
+ * 編排發票取消的完整流程
22
+ */
23
+ export class InvoiceCancellationContext {
24
+ constructor(
25
+ private repository: IInvoiceRepository,
26
+ private canceller: InvoiceCancellerRole
27
+ ) {}
28
+
29
+ async orchestrate(input: CancelInvoiceInput): Promise<CancelInvoiceOutput> {
30
+ // 1. 查詢發票
31
+ const invoice = await this.repository.findById(input.invoiceId)
32
+ if (!invoice) {
33
+ throw new InvoiceNotFoundError(input.invoiceId)
34
+ }
35
+
36
+ // 2. 驗證是否可取消
37
+ if (!invoice.statusObject.isIssued()) {
38
+ throw new InvalidCancellationError(`Cannot cancel ${invoice.statusObject.value} invoice`)
39
+ }
40
+
41
+ // 3. 執行取消
42
+ const cancelled = await this.canceller.cancel({
43
+ invoice,
44
+ reason: input.reason,
45
+ notes: input.notes,
46
+ })
47
+
48
+ // 4. 保存
49
+ await this.repository.save(cancelled)
50
+
51
+ // 5. 返回結果
52
+ return {
53
+ id: cancelled.id,
54
+ invoiceNumber: cancelled.invoiceNumberObject.value,
55
+ previousStatus: 'ISSUED',
56
+ newStatus: cancelled.statusObject.value,
57
+ cancelledAt: new Date(),
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,42 @@
1
+ import type { IInvoiceRepository } from '../../Domain/Contracts/IInvoiceRepository'
2
+ import type { Invoice } from '../../Domain/Entities/Invoice'
3
+ import { DuplicateInvoiceError } from '../../Domain/Errors/InvoiceError'
4
+ import type { InvoiceIssuerRole } from '../Roles/InvoiceIssuerRole'
5
+
6
+ export interface IssueInvoiceInput {
7
+ orderId: string
8
+ amount: number
9
+ currency?: string
10
+ buyerIdentifier?: string
11
+ carrierId?: string
12
+ }
13
+
14
+ /**
15
+ * 發票開立上下文 (DCI)
16
+ * 編排發票開立的完整流程
17
+ */
18
+ export class InvoiceIssuanceContext {
19
+ constructor(
20
+ private repository: IInvoiceRepository,
21
+ private issuer: InvoiceIssuerRole
22
+ ) {}
23
+
24
+ async orchestrate(input: IssueInvoiceInput): Promise<Invoice> {
25
+ // 1. 檢查訂單是否已有發票
26
+ const existing = await this.repository.findByOrderId(input.orderId)
27
+ if (existing) {
28
+ throw new DuplicateInvoiceError(input.orderId)
29
+ }
30
+
31
+ // 2. 生成新發票
32
+ const invoice = await this.issuer.issueInvoice({
33
+ ...input,
34
+ currency: input.currency ?? 'TWD',
35
+ })
36
+
37
+ // 3. 保存發票
38
+ await this.repository.save(invoice)
39
+
40
+ return invoice
41
+ }
42
+ }
@@ -0,0 +1,97 @@
1
+ import type { Invoice } from '../../Domain/Entities/Invoice'
2
+
3
+ export type CancellationReason =
4
+ | 'CUSTOMER_REQUEST'
5
+ | 'PAYMENT_FAILED'
6
+ | 'ORDER_CANCELLED'
7
+ | 'DUPLICATE'
8
+ | 'OTHER'
9
+
10
+ export interface CancelInvoiceInput {
11
+ invoice: Invoice
12
+ reason: CancellationReason
13
+ notes?: string
14
+ }
15
+
16
+ export interface CancellationRecord {
17
+ invoiceId: string
18
+ reason: CancellationReason
19
+ notes?: string
20
+ timestamp: Date
21
+ }
22
+
23
+ /**
24
+ * 發票取消者角色
25
+ */
26
+ export interface InvoiceCancellerRole {
27
+ cancel(input: CancelInvoiceInput): Promise<Invoice>
28
+ validateCancellationEligibility(invoice: Invoice): boolean
29
+ recordCancellationReason(
30
+ invoiceId: string,
31
+ reason: CancellationReason,
32
+ notes?: string
33
+ ): CancellationRecord
34
+ notifyRelatedServices(invoiceId: string): Promise<void>
35
+ }
36
+
37
+ /**
38
+ * 預設發票取消者實現
39
+ */
40
+ export class DefaultInvoiceCanceller implements InvoiceCancellerRole {
41
+ private cancellationRecords: Map<string, CancellationRecord> = new Map()
42
+
43
+ validateCancellationEligibility(invoice: Invoice): boolean {
44
+ return invoice.statusObject.canBeCancelled()
45
+ }
46
+
47
+ recordCancellationReason(
48
+ invoiceId: string,
49
+ reason: CancellationReason,
50
+ notes?: string
51
+ ): CancellationRecord {
52
+ const record: CancellationRecord = {
53
+ invoiceId,
54
+ reason,
55
+ notes,
56
+ timestamp: new Date(),
57
+ }
58
+ this.cancellationRecords.set(invoiceId, record)
59
+ return record
60
+ }
61
+
62
+ getCancellationRecord(invoiceId: string): CancellationRecord | undefined {
63
+ return this.cancellationRecords.get(invoiceId)
64
+ }
65
+
66
+ async notifyRelatedServices(invoiceId: string): Promise<void> {
67
+ const record = this.cancellationRecords.get(invoiceId)
68
+ if (!record) {
69
+ throw new Error(`No cancellation record found for invoice ${invoiceId}`)
70
+ }
71
+ // 實際實現應該透過 event bus 通知相關服務
72
+ // 這裡只是佔位符
73
+ }
74
+
75
+ async cancel(input: CancelInvoiceInput): Promise<Invoice> {
76
+ // 驗證是否可取消
77
+ if (!this.validateCancellationEligibility(input.invoice)) {
78
+ throw new Error(`Invoice ${input.invoice.id} cannot be cancelled`)
79
+ }
80
+
81
+ // 記錄取消
82
+ this.recordCancellationReason(input.invoice.id, input.reason, input.notes)
83
+
84
+ // 通知相關服務
85
+ await this.notifyRelatedServices(input.invoice.id)
86
+
87
+ // 執行取消
88
+ return input.invoice.cancel()
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 注入函式 - 用於 DI 容器
94
+ */
95
+ export function injectInvoiceCanceller(): InvoiceCancellerRole {
96
+ return new DefaultInvoiceCanceller()
97
+ }
@@ -0,0 +1,72 @@
1
+ import { Invoice } from '../../Domain/Entities/Invoice'
2
+ import { InvalidOrderIdError } from '../../Domain/Errors/InvoiceError'
3
+ import { InvoiceNumber } from '../../Domain/ValueObjects/InvoiceNumber'
4
+ import { InvoiceTax } from '../../Domain/ValueObjects/InvoiceTax'
5
+
6
+ export interface IssueInvoiceInput {
7
+ orderId: string
8
+ amount: number
9
+ currency?: string
10
+ buyerIdentifier?: string
11
+ carrierId?: string
12
+ }
13
+
14
+ export interface IssueInvoiceOutput {
15
+ invoice: Invoice
16
+ }
17
+
18
+ /**
19
+ * 發票發行者角色
20
+ */
21
+ export interface InvoiceIssuerRole {
22
+ issueInvoice(input: IssueInvoiceInput): Promise<Invoice>
23
+ generateInvoiceNumber(): InvoiceNumber
24
+ calculateTax(amount: number, rate?: number): InvoiceTax
25
+ validateOrderForInvoicing(orderId: string): Promise<boolean>
26
+ }
27
+
28
+ /**
29
+ * 預設發票發行者實現
30
+ */
31
+ export class DefaultInvoiceIssuer implements InvoiceIssuerRole {
32
+ generateInvoiceNumber(): InvoiceNumber {
33
+ return InvoiceNumber.generate()
34
+ }
35
+
36
+ calculateTax(amount: number, rate = 0.05): InvoiceTax {
37
+ return InvoiceTax.calculate(amount, rate)
38
+ }
39
+
40
+ async validateOrderForInvoicing(orderId: string): Promise<boolean> {
41
+ // 基本驗證 - 檢查 orderId 是否為有效字符串
42
+ return typeof orderId === 'string' && orderId.length > 0
43
+ }
44
+
45
+ async issueInvoice(input: IssueInvoiceInput): Promise<Invoice> {
46
+ // 驗證 orderId
47
+ if (!(await this.validateOrderForInvoicing(input.orderId))) {
48
+ throw new InvalidOrderIdError(input.orderId)
49
+ }
50
+
51
+ const invoiceNumber = this.generateInvoiceNumber()
52
+ const tax = this.calculateTax(input.amount, 0.05)
53
+
54
+ return Invoice.create({
55
+ orderId: input.orderId,
56
+ invoiceNumber: invoiceNumber.value,
57
+ amount: input.amount,
58
+ currency: input.currency ?? 'TWD',
59
+ tax: tax.amount,
60
+ taxRate: tax.rate,
61
+ buyerIdentifier: input.buyerIdentifier,
62
+ carrierId: input.carrierId,
63
+ })
64
+ }
65
+ }
66
+
67
+ /**
68
+ * 注入函式 - 用於 DI 容器
69
+ */
70
+ export function injectInvoiceIssuer(): InvoiceIssuerRole {
71
+ return new DefaultInvoiceIssuer()
72
+ }
@@ -0,0 +1,112 @@
1
+ import type { Invoice } from '../../Domain/Entities/Invoice'
2
+
3
+ export interface AuditLog {
4
+ invoiceId: string
5
+ action: string
6
+ timestamp: Date
7
+ details?: Record<string, any>
8
+ }
9
+
10
+ export interface ReportSummary {
11
+ total: number
12
+ issued: number
13
+ cancelled: number
14
+ returned: number
15
+ totalAmount: number
16
+ }
17
+
18
+ export interface ReportData {
19
+ summary: ReportSummary
20
+ invoices: Array<{
21
+ id: string
22
+ number: string
23
+ orderId: string
24
+ amount: number
25
+ status: string
26
+ createdAt: Date
27
+ }>
28
+ }
29
+
30
+ /**
31
+ * 發票追蹤者角色
32
+ */
33
+ export interface InvoiceTrackerRole {
34
+ trackStatus(invoice: Invoice): Promise<void>
35
+ trackStatus(invoiceId: string): Promise<void>
36
+ trackInvoiceStatus(invoiceId: string): Promise<string | null>
37
+ generateReport(startDate: Date, endDate: Date): Promise<ReportData>
38
+ recordAuditLog(log: AuditLog): void
39
+ auditTrail(invoiceId: string): Promise<AuditLog[]>
40
+ trackInvoice(invoice: Invoice): void
41
+ }
42
+
43
+ /**
44
+ * 預設發票追蹤者實現
45
+ */
46
+ export class DefaultInvoiceTracker implements InvoiceTrackerRole {
47
+ private invoices: Map<string, Invoice> = new Map()
48
+ private auditLogs: Map<string, AuditLog[]> = new Map()
49
+
50
+ trackInvoice(invoice: Invoice): void {
51
+ this.invoices.set(invoice.id, invoice)
52
+ }
53
+
54
+ async trackStatus(invoiceOrId: Invoice | string): Promise<void> {
55
+ // 接受 Invoice 或 string(invoiceId)
56
+ if (typeof invoiceOrId === 'string') {
57
+ // 如果是字符串,暫時不做任何操作
58
+ return
59
+ }
60
+ // 追蹤發票狀態
61
+ this.trackInvoice(invoiceOrId)
62
+ }
63
+
64
+ async trackInvoiceStatus(invoiceId: string): Promise<string | null> {
65
+ const invoice = this.invoices.get(invoiceId)
66
+ return invoice ? invoice.status : null
67
+ }
68
+
69
+ recordAuditLog(log: AuditLog): void {
70
+ if (!this.auditLogs.has(log.invoiceId)) {
71
+ this.auditLogs.set(log.invoiceId, [])
72
+ }
73
+ this.auditLogs.get(log.invoiceId)?.push(log)
74
+ }
75
+
76
+ async auditTrail(invoiceId: string): Promise<AuditLog[]> {
77
+ return this.auditLogs.get(invoiceId) ?? []
78
+ }
79
+
80
+ async generateReport(startDate: Date, endDate: Date): Promise<ReportData> {
81
+ const invoices = Array.from(this.invoices.values()).filter(
82
+ (inv) => inv.createdAt >= startDate && inv.createdAt <= endDate
83
+ )
84
+
85
+ const summary: ReportSummary = {
86
+ total: invoices.length,
87
+ issued: invoices.filter((inv) => inv.statusObject.isIssued()).length,
88
+ cancelled: invoices.filter((inv) => inv.statusObject.isCancelled()).length,
89
+ returned: invoices.filter((inv) => inv.statusObject.isReturned()).length,
90
+ totalAmount: invoices.reduce((sum, inv) => sum + inv.amountObject.value, 0),
91
+ }
92
+
93
+ return {
94
+ summary,
95
+ invoices: invoices.map((inv) => ({
96
+ id: inv.id,
97
+ number: inv.invoiceNumberObject.value,
98
+ orderId: inv.orderId,
99
+ amount: inv.amountObject.value,
100
+ status: inv.statusObject.value,
101
+ createdAt: inv.createdAt,
102
+ })),
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 注入函式 - 用於 DI 容器
109
+ */
110
+ export function injectInvoiceTracker(): InvoiceTrackerRole {
111
+ return new DefaultInvoiceTracker()
112
+ }
@@ -0,0 +1,32 @@
1
+ import { UseCase } from '@gravito/enterprise'
2
+ import type { IInvoiceRepository } from '../../Domain/Contracts/IInvoiceRepository'
3
+ import {
4
+ type CancelInvoiceInput,
5
+ type CancelInvoiceOutput,
6
+ InvoiceCancellationContext,
7
+ } from '../Contexts/InvoiceCancellationContext'
8
+ import { type CancellationReason, DefaultInvoiceCanceller } from '../Roles/InvoiceCancellerRole'
9
+
10
+ export interface CancelInvoiceUseCaseInput {
11
+ invoiceId: string
12
+ reason: CancellationReason
13
+ notes?: string
14
+ }
15
+
16
+ /**
17
+ * 發票取消 UseCase(薄殼委派)
18
+ * 委派到 InvoiceCancellationContext 進行實際業務流程處理
19
+ */
20
+ export class CancelInvoice extends UseCase<CancelInvoiceUseCaseInput, CancelInvoiceOutput> {
21
+ private context: InvoiceCancellationContext
22
+
23
+ constructor(repository: IInvoiceRepository) {
24
+ super()
25
+ const canceller = new DefaultInvoiceCanceller()
26
+ this.context = new InvoiceCancellationContext(repository, canceller)
27
+ }
28
+
29
+ async execute(input: CancelInvoiceUseCaseInput): Promise<CancelInvoiceOutput> {
30
+ return this.context.orchestrate(input as CancelInvoiceInput)
31
+ }
32
+ }
@@ -1,6 +1,8 @@
1
1
  import { UseCase } from '@gravito/enterprise'
2
2
  import type { IInvoiceRepository } from '../../Domain/Contracts/IInvoiceRepository'
3
- import { Invoice } from '../../Domain/Entities/Invoice'
3
+ import type { Invoice } from '../../Domain/Entities/Invoice'
4
+ import { InvoiceIssuanceContext } from '../Contexts/InvoiceIssuanceContext'
5
+ import { DefaultInvoiceIssuer } from '../Roles/InvoiceIssuerRole'
4
6
 
5
7
  export interface IssueInvoiceInput {
6
8
  orderId: string
@@ -9,35 +11,20 @@ export interface IssueInvoiceInput {
9
11
  carrierId?: string
10
12
  }
11
13
 
14
+ /**
15
+ * 發票開立 UseCase(薄殼委派)
16
+ * 委派到 InvoiceIssuanceContext 進行實際業務流程處理
17
+ */
12
18
  export class IssueInvoice extends UseCase<IssueInvoiceInput, Invoice> {
13
- constructor(private repository: IInvoiceRepository) {
19
+ private context: InvoiceIssuanceContext
20
+
21
+ constructor(repository: IInvoiceRepository) {
14
22
  super()
23
+ const issuer = new DefaultInvoiceIssuer()
24
+ this.context = new InvoiceIssuanceContext(repository, issuer)
15
25
  }
16
26
 
17
27
  async execute(input: IssueInvoiceInput): Promise<Invoice> {
18
- // 檢查是否已開立過
19
- const existing = await this.repository.findByOrderId(input.orderId)
20
- if (existing) {
21
- return existing
22
- }
23
-
24
- // 模擬發票號碼產生器
25
- const randomNum = Math.floor(10000000 + Math.random() * 90000000)
26
- const invoiceNumber = `GX-${randomNum}`
27
-
28
- const tax = Math.round(input.amount * 0.05)
29
-
30
- const invoice = Invoice.create({
31
- orderId: input.orderId,
32
- invoiceNumber,
33
- amount: input.amount,
34
- tax,
35
- status: 'ISSUED',
36
- buyerIdentifier: input.buyerIdentifier,
37
- carrierId: input.carrierId,
38
- })
39
-
40
- await this.repository.save(invoice)
41
- return invoice
28
+ return this.context.orchestrate(input)
42
29
  }
43
30
  }
@@ -0,0 +1,56 @@
1
+ import { UseCase } from '@gravito/enterprise'
2
+ import type { IInvoiceRepository } from '../../Domain/Contracts/IInvoiceRepository'
3
+ import { InvoiceAuditContext } from '../Contexts/InvoiceAuditContext'
4
+ import { DefaultInvoiceTracker } from '../Roles/InvoiceTrackerRole'
5
+
6
+ export interface QueryInvoiceStatusInput {
7
+ invoiceId: string
8
+ }
9
+
10
+ export interface QueryInvoiceStatusOutput {
11
+ id: string
12
+ invoiceNumber: string
13
+ orderId: string
14
+ status: string
15
+ amount: number
16
+ createdAt: Date
17
+ }
18
+
19
+ export interface GenerateInvoiceReportInput {
20
+ startDate: Date
21
+ endDate: Date
22
+ }
23
+
24
+ /**
25
+ * 發票查詢 UseCase(薄殼委派)
26
+ */
27
+ export class QueryInvoiceStatus extends UseCase<QueryInvoiceStatusInput, QueryInvoiceStatusOutput> {
28
+ private context: InvoiceAuditContext
29
+
30
+ constructor(repository: IInvoiceRepository) {
31
+ super()
32
+ const tracker = new DefaultInvoiceTracker()
33
+ this.context = new InvoiceAuditContext(repository, tracker)
34
+ }
35
+
36
+ async execute(input: QueryInvoiceStatusInput): Promise<QueryInvoiceStatusOutput> {
37
+ return this.context.queryStatus(input)
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 發票報告生成 UseCase(薄殼委派)
43
+ */
44
+ export class GenerateInvoiceReport extends UseCase<GenerateInvoiceReportInput, any> {
45
+ private context: InvoiceAuditContext
46
+
47
+ constructor(repository: IInvoiceRepository) {
48
+ super()
49
+ const tracker = new DefaultInvoiceTracker()
50
+ this.context = new InvoiceAuditContext(repository, tracker)
51
+ }
52
+
53
+ async execute(input: GenerateInvoiceReportInput): Promise<any> {
54
+ return this.context.generateReport(input)
55
+ }
56
+ }
@@ -5,4 +5,7 @@ export interface IInvoiceRepository {
5
5
  findById(id: string): Promise<Invoice | null>
6
6
  findByOrderId(orderId: string): Promise<Invoice | null>
7
7
  findAll(): Promise<Invoice[]>
8
+ findByInvoiceNumber(invoiceNumber: string): Promise<Invoice | null>
9
+ findByStatus(status: string): Promise<Invoice[]>
10
+ findByDateRange(startDate: Date, endDate: Date): Promise<Invoice[]>
8
11
  }