@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 ADDED
@@ -0,0 +1,10 @@
1
+ # @gravito/satellite-invoice
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - @gravito/atlas@1.0.1
9
+ - @gravito/core@1.0.0
10
+ - @gravito/enterprise@1.0.0
@@ -0,0 +1,8 @@
1
+ import { ServiceProvider, Container } from '@gravito/core';
2
+
3
+ declare class InvoiceServiceProvider extends ServiceProvider {
4
+ register(container: Container): void;
5
+ boot(): void;
6
+ }
7
+
8
+ export { InvoiceServiceProvider };
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
+ }