@gravito/satellite-invoice 0.1.1
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 +10 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +177 -0
- package/package.json +29 -0
- package/src/Application/UseCases/IssueInvoice.ts +43 -0
- package/src/Domain/Contracts/IInvoiceRepository.ts +8 -0
- package/src/Domain/Entities/Invoice.ts +53 -0
- package/src/Infrastructure/Persistence/AtlasInvoiceRepository.ts +38 -0
- package/src/Interface/Http/Controllers/AdminInvoiceController.ts +32 -0
- package/src/index.ts +56 -0
- package/tsconfig.json +21 -0
package/CHANGELOG.md
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { ServiceProvider } from "@gravito/core";
|
|
3
|
+
|
|
4
|
+
// src/Application/UseCases/IssueInvoice.ts
|
|
5
|
+
import { UseCase } from "@gravito/enterprise";
|
|
6
|
+
|
|
7
|
+
// src/Domain/Entities/Invoice.ts
|
|
8
|
+
import { Entity } from "@gravito/enterprise";
|
|
9
|
+
var Invoice = class _Invoice extends Entity {
|
|
10
|
+
_props;
|
|
11
|
+
constructor(props, id) {
|
|
12
|
+
super(id || crypto.randomUUID());
|
|
13
|
+
this._props = props;
|
|
14
|
+
}
|
|
15
|
+
get orderId() {
|
|
16
|
+
return this._props.orderId;
|
|
17
|
+
}
|
|
18
|
+
get invoiceNumber() {
|
|
19
|
+
return this._props.invoiceNumber;
|
|
20
|
+
}
|
|
21
|
+
get amount() {
|
|
22
|
+
return this._props.amount;
|
|
23
|
+
}
|
|
24
|
+
get status() {
|
|
25
|
+
return this._props.status;
|
|
26
|
+
}
|
|
27
|
+
static create(props, id) {
|
|
28
|
+
return new _Invoice(
|
|
29
|
+
{
|
|
30
|
+
...props,
|
|
31
|
+
status: props.status || "ISSUED",
|
|
32
|
+
createdAt: props.createdAt || /* @__PURE__ */ new Date()
|
|
33
|
+
},
|
|
34
|
+
id
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
unpack() {
|
|
38
|
+
return { ...this._props };
|
|
39
|
+
}
|
|
40
|
+
cancel() {
|
|
41
|
+
this._props.status = "CANCELLED";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/Application/UseCases/IssueInvoice.ts
|
|
46
|
+
var IssueInvoice = class extends UseCase {
|
|
47
|
+
constructor(repository) {
|
|
48
|
+
super();
|
|
49
|
+
this.repository = repository;
|
|
50
|
+
}
|
|
51
|
+
async execute(input) {
|
|
52
|
+
const existing = await this.repository.findByOrderId(input.orderId);
|
|
53
|
+
if (existing) {
|
|
54
|
+
return existing;
|
|
55
|
+
}
|
|
56
|
+
const randomNum = Math.floor(1e7 + Math.random() * 9e7);
|
|
57
|
+
const invoiceNumber = `GX-${randomNum}`;
|
|
58
|
+
const tax = Math.round(input.amount * 0.05);
|
|
59
|
+
const invoice = Invoice.create({
|
|
60
|
+
orderId: input.orderId,
|
|
61
|
+
invoiceNumber,
|
|
62
|
+
amount: input.amount,
|
|
63
|
+
tax,
|
|
64
|
+
status: "ISSUED",
|
|
65
|
+
buyerIdentifier: input.buyerIdentifier,
|
|
66
|
+
carrierId: input.carrierId
|
|
67
|
+
});
|
|
68
|
+
await this.repository.save(invoice);
|
|
69
|
+
return invoice;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// src/Infrastructure/Persistence/AtlasInvoiceRepository.ts
|
|
74
|
+
import { Model } from "@gravito/atlas";
|
|
75
|
+
var InvoiceModel = class extends Model {
|
|
76
|
+
static table = "invoices";
|
|
77
|
+
};
|
|
78
|
+
var AtlasInvoiceRepository = class {
|
|
79
|
+
async save(invoice) {
|
|
80
|
+
const data = {
|
|
81
|
+
id: invoice.id,
|
|
82
|
+
...invoice.unpack()
|
|
83
|
+
};
|
|
84
|
+
const existing = await InvoiceModel.find(invoice.id);
|
|
85
|
+
if (existing) {
|
|
86
|
+
await InvoiceModel.query().where("id", invoice.id).update(data);
|
|
87
|
+
} else {
|
|
88
|
+
await InvoiceModel.query().insert(data);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async findById(id) {
|
|
92
|
+
const row = await InvoiceModel.find(id);
|
|
93
|
+
return row ? Invoice.create(row.props, row.props.id) : null;
|
|
94
|
+
}
|
|
95
|
+
async findByOrderId(orderId) {
|
|
96
|
+
const row = await InvoiceModel.query().where("order_id", orderId).first();
|
|
97
|
+
return row ? Invoice.create(row.props, row.props.id) : null;
|
|
98
|
+
}
|
|
99
|
+
async findAll() {
|
|
100
|
+
const rows = await InvoiceModel.all();
|
|
101
|
+
return rows.map((row) => Invoice.create(row.props, row.props.id));
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/Interface/Http/Controllers/AdminInvoiceController.ts
|
|
106
|
+
var AdminInvoiceController = class {
|
|
107
|
+
constructor(core) {
|
|
108
|
+
this.core = core;
|
|
109
|
+
}
|
|
110
|
+
async index(ctx) {
|
|
111
|
+
const repo = this.core.container.make("invoice.repository");
|
|
112
|
+
const invoices = await repo.findAll();
|
|
113
|
+
return ctx.json(
|
|
114
|
+
invoices.map((inv) => ({
|
|
115
|
+
id: inv.id,
|
|
116
|
+
...inv.unpack()
|
|
117
|
+
}))
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
async store(ctx) {
|
|
121
|
+
const body = await ctx.req.json();
|
|
122
|
+
const useCase = this.core.container.make("invoice.usecase.issue");
|
|
123
|
+
const invoice = await useCase.execute(body);
|
|
124
|
+
return ctx.json({
|
|
125
|
+
success: true,
|
|
126
|
+
data: {
|
|
127
|
+
id: invoice.id,
|
|
128
|
+
number: invoice.invoiceNumber
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// src/index.ts
|
|
135
|
+
var InvoiceServiceProvider = class extends ServiceProvider {
|
|
136
|
+
register(container) {
|
|
137
|
+
container.singleton("invoice.repository", () => new AtlasInvoiceRepository());
|
|
138
|
+
container.bind(
|
|
139
|
+
"invoice.usecase.issue",
|
|
140
|
+
() => new IssueInvoice(container.make("invoice.repository"))
|
|
141
|
+
);
|
|
142
|
+
container.singleton("invoice.controller.admin", () => new AdminInvoiceController(this.core));
|
|
143
|
+
}
|
|
144
|
+
boot() {
|
|
145
|
+
const core = this.core;
|
|
146
|
+
if (!core) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
core.logger.info("\u{1F9FE} Invoice Satellite is ready");
|
|
150
|
+
const controller = core.container.make("invoice.controller.admin");
|
|
151
|
+
core.router.prefix("/api/admin/v1/invoices").group((router) => {
|
|
152
|
+
router.get("/", (ctx) => controller.index(ctx));
|
|
153
|
+
router.post("/", (ctx) => controller.store(ctx));
|
|
154
|
+
});
|
|
155
|
+
core.hooks.addAction(
|
|
156
|
+
"order:paid",
|
|
157
|
+
async (payload) => {
|
|
158
|
+
core.logger.info(`[Invoice] Triggering auto-issuance for order: ${payload.orderId}`);
|
|
159
|
+
const issueUseCase = core.container.make("invoice.usecase.issue");
|
|
160
|
+
try {
|
|
161
|
+
const invoice = await issueUseCase.execute({
|
|
162
|
+
orderId: payload.orderId,
|
|
163
|
+
amount: payload.amount,
|
|
164
|
+
buyerIdentifier: payload.buyer?.identifier,
|
|
165
|
+
carrierId: payload.buyer?.carrierId
|
|
166
|
+
});
|
|
167
|
+
core.logger.info(`[Invoice] Automatically issued: ${invoice.invoiceNumber}`);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
core.logger.error(`[Invoice] Auto-issue failed: ${error.message}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
export {
|
|
176
|
+
InvoiceServiceProvider
|
|
177
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gravito/satellite-invoice",
|
|
3
|
+
"version": "0.1.1",
|
|
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,43 @@
|
|
|
1
|
+
import { UseCase } from '@gravito/enterprise'
|
|
2
|
+
import type { IInvoiceRepository } from '../../Domain/Contracts/IInvoiceRepository'
|
|
3
|
+
import { Invoice } from '../../Domain/Entities/Invoice'
|
|
4
|
+
|
|
5
|
+
export interface IssueInvoiceInput {
|
|
6
|
+
orderId: string
|
|
7
|
+
amount: number
|
|
8
|
+
buyerIdentifier?: string
|
|
9
|
+
carrierId?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class IssueInvoice extends UseCase<IssueInvoiceInput, Invoice> {
|
|
13
|
+
constructor(private repository: IInvoiceRepository) {
|
|
14
|
+
super()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
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
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Invoice } from '../Entities/Invoice'
|
|
2
|
+
|
|
3
|
+
export interface IInvoiceRepository {
|
|
4
|
+
save(invoice: Invoice): Promise<void>
|
|
5
|
+
findById(id: string): Promise<Invoice | null>
|
|
6
|
+
findByOrderId(orderId: string): Promise<Invoice | null>
|
|
7
|
+
findAll(): Promise<Invoice[]>
|
|
8
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Entity } from '@gravito/enterprise'
|
|
2
|
+
|
|
3
|
+
export interface InvoiceProps {
|
|
4
|
+
orderId: string
|
|
5
|
+
invoiceNumber: string
|
|
6
|
+
amount: number
|
|
7
|
+
tax: number
|
|
8
|
+
status: 'ISSUED' | 'CANCELLED' | 'RETURNED'
|
|
9
|
+
buyerIdentifier?: string
|
|
10
|
+
carrierId?: string
|
|
11
|
+
createdAt?: Date
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Invoice extends Entity<string> {
|
|
15
|
+
private _props: InvoiceProps
|
|
16
|
+
|
|
17
|
+
private constructor(props: InvoiceProps, id?: string) {
|
|
18
|
+
super(id || crypto.randomUUID())
|
|
19
|
+
this._props = props
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get orderId() {
|
|
23
|
+
return this._props.orderId
|
|
24
|
+
}
|
|
25
|
+
get invoiceNumber() {
|
|
26
|
+
return this._props.invoiceNumber
|
|
27
|
+
}
|
|
28
|
+
get amount() {
|
|
29
|
+
return this._props.amount
|
|
30
|
+
}
|
|
31
|
+
get status() {
|
|
32
|
+
return this._props.status
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static create(props: InvoiceProps, id?: string) {
|
|
36
|
+
return new Invoice(
|
|
37
|
+
{
|
|
38
|
+
...props,
|
|
39
|
+
status: props.status || 'ISSUED',
|
|
40
|
+
createdAt: props.createdAt || new Date(),
|
|
41
|
+
},
|
|
42
|
+
id
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
unpack(): InvoiceProps {
|
|
47
|
+
return { ...this._props }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
cancel() {
|
|
51
|
+
this._props.status = 'CANCELLED'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Model } from '@gravito/atlas'
|
|
2
|
+
import type { IInvoiceRepository } from '../../Domain/Contracts/IInvoiceRepository'
|
|
3
|
+
import { Invoice } from '../../Domain/Entities/Invoice'
|
|
4
|
+
|
|
5
|
+
class InvoiceModel extends Model {
|
|
6
|
+
static override table = 'invoices'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class AtlasInvoiceRepository implements IInvoiceRepository {
|
|
10
|
+
async save(invoice: Invoice): Promise<void> {
|
|
11
|
+
const data = {
|
|
12
|
+
id: invoice.id,
|
|
13
|
+
...invoice.unpack(),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const existing = await InvoiceModel.find(invoice.id)
|
|
17
|
+
if (existing) {
|
|
18
|
+
await (InvoiceModel.query() as any).where('id', invoice.id).update(data)
|
|
19
|
+
} else {
|
|
20
|
+
await (InvoiceModel.query() as any).insert(data)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async findById(id: string): Promise<Invoice | null> {
|
|
25
|
+
const row = await InvoiceModel.find(id)
|
|
26
|
+
return row ? Invoice.create((row as any).props, (row as any).props.id) : null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async findByOrderId(orderId: string): Promise<Invoice | null> {
|
|
30
|
+
const row = await (InvoiceModel.query() as any).where('order_id', orderId).first()
|
|
31
|
+
return row ? Invoice.create(row.props, row.props.id) : null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async findAll(): Promise<Invoice[]> {
|
|
35
|
+
const rows = await InvoiceModel.all()
|
|
36
|
+
return rows.map((row) => Invoice.create((row as any).props, (row as any).props.id))
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PlanetCore } from '@gravito/core'
|
|
2
|
+
import type { IssueInvoice } from '../../../Application/UseCases/IssueInvoice'
|
|
3
|
+
import type { Invoice } from '../../../Domain/Entities/Invoice'
|
|
4
|
+
|
|
5
|
+
export class AdminInvoiceController {
|
|
6
|
+
constructor(private core: PlanetCore) {}
|
|
7
|
+
|
|
8
|
+
async index(ctx: any) {
|
|
9
|
+
// 獲取所有發票清單
|
|
10
|
+
const repo = this.core.container.make('invoice.repository') as any
|
|
11
|
+
const invoices = await repo.findAll()
|
|
12
|
+
return ctx.json(
|
|
13
|
+
invoices.map((inv: Invoice) => ({
|
|
14
|
+
id: inv.id,
|
|
15
|
+
...inv.unpack(),
|
|
16
|
+
}))
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async store(ctx: any) {
|
|
21
|
+
const body = await ctx.req.json()
|
|
22
|
+
const useCase = this.core.container.make<IssueInvoice>('invoice.usecase.issue')
|
|
23
|
+
const invoice = await useCase.execute(body)
|
|
24
|
+
return ctx.json({
|
|
25
|
+
success: true,
|
|
26
|
+
data: {
|
|
27
|
+
id: invoice.id,
|
|
28
|
+
number: invoice.invoiceNumber,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type Container, ServiceProvider } from '@gravito/core'
|
|
2
|
+
import { IssueInvoice } from './Application/UseCases/IssueInvoice'
|
|
3
|
+
import { AtlasInvoiceRepository } from './Infrastructure/Persistence/AtlasInvoiceRepository'
|
|
4
|
+
import { AdminInvoiceController } from './Interface/Http/Controllers/AdminInvoiceController'
|
|
5
|
+
|
|
6
|
+
export class InvoiceServiceProvider extends ServiceProvider {
|
|
7
|
+
register(container: Container): void {
|
|
8
|
+
container.singleton('invoice.repository', () => new AtlasInvoiceRepository())
|
|
9
|
+
container.bind(
|
|
10
|
+
'invoice.usecase.issue',
|
|
11
|
+
() => new IssueInvoice(container.make('invoice.repository'))
|
|
12
|
+
)
|
|
13
|
+
container.singleton('invoice.controller.admin', () => new AdminInvoiceController(this.core!))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override boot(): void {
|
|
17
|
+
const core = this.core
|
|
18
|
+
if (!core) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
core.logger.info('🧾 Invoice Satellite is ready')
|
|
23
|
+
|
|
24
|
+
const controller = core.container.make<AdminInvoiceController>('invoice.controller.admin')
|
|
25
|
+
|
|
26
|
+
// 註冊管理路由
|
|
27
|
+
core.router.prefix('/api/admin/v1/invoices').group((router) => {
|
|
28
|
+
router.get('/', (ctx) => controller.index(ctx))
|
|
29
|
+
router.post('/', (ctx) => controller.store(ctx))
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 自動化 Hook: 支付成功後自動開票
|
|
34
|
+
*/
|
|
35
|
+
core.hooks.addAction(
|
|
36
|
+
'order:paid',
|
|
37
|
+
async (payload: { orderId: string; amount: number; buyer?: any }) => {
|
|
38
|
+
core.logger.info(`[Invoice] Triggering auto-issuance for order: ${payload.orderId}`)
|
|
39
|
+
|
|
40
|
+
const issueUseCase = core.container.make<IssueInvoice>('invoice.usecase.issue')
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const invoice = await issueUseCase.execute({
|
|
44
|
+
orderId: payload.orderId,
|
|
45
|
+
amount: payload.amount,
|
|
46
|
+
buyerIdentifier: payload.buyer?.identifier,
|
|
47
|
+
carrierId: payload.buyer?.carrierId,
|
|
48
|
+
})
|
|
49
|
+
core.logger.info(`[Invoice] Automatically issued: ${invoice.invoiceNumber}`)
|
|
50
|
+
} catch (error: any) {
|
|
51
|
+
core.logger.error(`[Invoice] Auto-issue failed: ${error.message}`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"baseUrl": ".",
|
|
6
|
+
"paths": {
|
|
7
|
+
"@gravito/core": [
|
|
8
|
+
"../../packages/core/src/index.ts"
|
|
9
|
+
],
|
|
10
|
+
"@gravito/*": [
|
|
11
|
+
"../../packages/*/src/index.ts"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"types": [
|
|
15
|
+
"bun-types"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"include": [
|
|
19
|
+
"src/**/*"
|
|
20
|
+
]
|
|
21
|
+
}
|