@gravito/satellite-commerce 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/.dockerignore +8 -0
- package/.env.example +3 -0
- package/ARCHITECTURE.md +40 -0
- package/CHANGELOG.md +12 -0
- package/Dockerfile +25 -0
- package/README.md +42 -0
- package/WHITEPAPER.md +37 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +424 -0
- package/package.json +33 -0
- package/src/Application/Services/AdjustmentCalculator.ts +34 -0
- package/src/Application/Services/ProductResolver.ts +46 -0
- package/src/Application/Subscribers/RewardSubscriber.ts +27 -0
- package/src/Application/UseCases/AdminListOrders.ts +34 -0
- package/src/Application/UseCases/PlaceOrder.ts +122 -0
- package/src/Domain/Entities/Commerce.ts +26 -0
- package/src/Domain/Entities/Order.ts +164 -0
- package/src/Infrastructure/Persistence/Migrations/20250101_create_commerce_tables.ts +64 -0
- package/src/Interface/Http/Controllers/AdminOrderController.ts +21 -0
- package/src/Interface/Http/Controllers/CheckoutController.ts +50 -0
- package/src/Interface/Http/Requests/PlaceOrderRequest.ts +18 -0
- package/src/index.ts +63 -0
- package/src/manifest.json +12 -0
- package/tests/grand-review.ts +153 -0
- package/tests/launchpad-ignition.ts +41 -0
- package/tests/unit.test.ts +7 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { DB, Schema } from '@gravito/atlas'
|
|
2
|
+
import { PlanetCore, setApp } from '@gravito/core'
|
|
3
|
+
import type { PlaceOrder } from '../src/Application/UseCases/PlaceOrder'
|
|
4
|
+
import { CommerceServiceProvider } from '../src/index'
|
|
5
|
+
|
|
6
|
+
async function grandReview() {
|
|
7
|
+
console.log('\n🏎️ [Grand Review] 啟動 Commerce 原子性與樂觀鎖校閱...')
|
|
8
|
+
|
|
9
|
+
// 1. 初始化核心
|
|
10
|
+
const core = await PlanetCore.boot({
|
|
11
|
+
config: {
|
|
12
|
+
'database.default': 'sqlite',
|
|
13
|
+
'database.connections.sqlite': { driver: 'sqlite', database: ':memory:' },
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// 模擬註冊 Cache 服務,以便 PlaceOrder 運作
|
|
18
|
+
core.container.singleton('cache', () => {
|
|
19
|
+
const store = new Map()
|
|
20
|
+
return {
|
|
21
|
+
get: async (key: string) => store.get(key),
|
|
22
|
+
put: async (key: string, val: any) => store.set(key, val),
|
|
23
|
+
has: async (key: string) => store.has(key),
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
setApp(core)
|
|
28
|
+
|
|
29
|
+
DB.addConnection('default', { driver: 'sqlite', database: ':memory:' })
|
|
30
|
+
|
|
31
|
+
await Schema.create('product_variants', (table) => {
|
|
32
|
+
table.string('id').primary()
|
|
33
|
+
table.string('sku')
|
|
34
|
+
table.string('name')
|
|
35
|
+
table.decimal('price', 15, 2)
|
|
36
|
+
table.integer('stock')
|
|
37
|
+
table.integer('version').default(1)
|
|
38
|
+
table.timestamp('updated_at').nullable()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const migration = await import(
|
|
42
|
+
'../src/Infrastructure/Persistence/Migrations/20250101_create_commerce_tables'
|
|
43
|
+
)
|
|
44
|
+
await migration.default.up()
|
|
45
|
+
|
|
46
|
+
await core.use(new CommerceServiceProvider())
|
|
47
|
+
await core.bootstrap()
|
|
48
|
+
|
|
49
|
+
const placeOrder = core.container.make<PlaceOrder>('commerce.place-order')
|
|
50
|
+
|
|
51
|
+
console.log('\n🧪 [Test A] 執行標準下單...')
|
|
52
|
+
await DB.table('product_variants').insert({
|
|
53
|
+
id: 'v1',
|
|
54
|
+
sku: 'TSHIRT',
|
|
55
|
+
name: 'Cool T-Shirt',
|
|
56
|
+
price: 500,
|
|
57
|
+
stock: 10,
|
|
58
|
+
version: 1,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const result = await placeOrder.execute({
|
|
62
|
+
memberId: 'm1',
|
|
63
|
+
items: [{ variantId: 'v1', quantity: 2 }],
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const updatedVariant = (await DB.table('product_variants').where('id', 'v1').first()) as any
|
|
67
|
+
if (updatedVariant) {
|
|
68
|
+
console.log(
|
|
69
|
+
`✅ 訂單已建立: ${result.orderId}, 剩餘庫存: ${updatedVariant.stock}, 版本: ${updatedVariant.version}`
|
|
70
|
+
)
|
|
71
|
+
if (updatedVariant.stock !== 8) {
|
|
72
|
+
throw new Error('Stock deduction error')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('\n🧪 [Test B] 模擬兩個人同時搶購最後 5 件商品...')
|
|
77
|
+
const attempt1 = placeOrder.execute({
|
|
78
|
+
memberId: 'user_a',
|
|
79
|
+
items: [{ variantId: 'v1', quantity: 5 }],
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const attempt2 = placeOrder.execute({
|
|
83
|
+
memberId: 'user_b',
|
|
84
|
+
items: [{ variantId: 'v1', quantity: 5 }],
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const [res1, res2] = await Promise.allSettled([attempt1, attempt2])
|
|
88
|
+
|
|
89
|
+
console.log('🏁 搶購結果:')
|
|
90
|
+
console.log(
|
|
91
|
+
`👤 用戶 A: ${res1.status === 'fulfilled' ? '✅ 成功' : `❌ 失敗: ${(res1 as any).reason.message}`}`
|
|
92
|
+
)
|
|
93
|
+
console.log(
|
|
94
|
+
`👤 用戶 B: ${res2.status === 'fulfilled' ? '✅ 成功' : `❌ 失敗: ${(res2 as any).reason.message}`}`
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const finalVariant = (await DB.table('product_variants').where('id', 'v1').first()) as any
|
|
98
|
+
if (finalVariant) {
|
|
99
|
+
console.log(`📊 最終庫存: ${finalVariant.stock} (預期應為 3, 且無負數)`)
|
|
100
|
+
if (finalVariant.stock < 0) {
|
|
101
|
+
throw new Error('Overselling detected!')
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log('\n🧪 [Test C] 驗證 Stage 2 (Sport Mode) 內存加速...')
|
|
106
|
+
// 切換環境變數為 sport
|
|
107
|
+
process.env.COMMERCE_MODE = 'sport'
|
|
108
|
+
|
|
109
|
+
// 1. 建立新商品並下單 (這會寫入快取)
|
|
110
|
+
await DB.table('product_variants').insert({
|
|
111
|
+
id: 'v_sport',
|
|
112
|
+
sku: 'SPORT',
|
|
113
|
+
name: 'Speed Shoes',
|
|
114
|
+
price: 1000,
|
|
115
|
+
stock: 10,
|
|
116
|
+
version: 1,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
await placeOrder.execute({
|
|
120
|
+
memberId: 'm1',
|
|
121
|
+
items: [{ variantId: 'v_sport', quantity: 1 }],
|
|
122
|
+
})
|
|
123
|
+
console.log('✅ 第一次下單成功,已 priming 快取')
|
|
124
|
+
|
|
125
|
+
// 2. 故意從資料庫刪除該商品元數據 (但保留 ID 供庫存扣減)
|
|
126
|
+
// 在 Sport 模式下,ProductResolver 應該從內存讀取名稱,不會因為 DB 沒名稱而失敗
|
|
127
|
+
await DB.table('product_variants').where('id', 'v_sport').update({ name: 'CLEARED_IN_DB' })
|
|
128
|
+
|
|
129
|
+
const sportResult = await placeOrder.execute({
|
|
130
|
+
memberId: 'm1',
|
|
131
|
+
items: [{ variantId: 'v_sport', quantity: 1 }],
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// 檢查快照名稱是否維持原樣 (證明來自快取)
|
|
135
|
+
const orderItem = (await DB.table('order_items')
|
|
136
|
+
.where('order_id', sportResult.orderId)
|
|
137
|
+
.first()) as any
|
|
138
|
+
console.log(
|
|
139
|
+
`📊 快取驗證: 訂單內商品名稱 = "${orderItem.name}" (預期應為 "Speed Shoes", 而非 "CLEARED_IN_DB")`
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if (orderItem.name !== 'Speed Shoes') {
|
|
143
|
+
throw new Error('Cache was not utilized in Sport mode!')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log('\n🎉 [Grand Review] 所有模式 (Standard, Sport, Hooks) 校閱成功!')
|
|
147
|
+
process.exit(0)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
grandReview().catch((err) => {
|
|
151
|
+
console.error('💥 校閱失敗:', err)
|
|
152
|
+
process.exit(1)
|
|
153
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { PlanetCore } from '@gravito/core'
|
|
2
|
+
import { CommerceServiceProvider } from '../src/index'
|
|
3
|
+
|
|
4
|
+
async function launchpadIgnition() {
|
|
5
|
+
console.log('🚀 [Launchpad] 正在模擬衛星 "commerce" 的容器化點火...')
|
|
6
|
+
|
|
7
|
+
// 模擬 Launchpad 注入環境變數 (Stage 2 Sport Mode)
|
|
8
|
+
process.env.COMMERCE_MODE = 'sport'
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const core = await PlanetCore.boot({
|
|
12
|
+
config: {
|
|
13
|
+
'database.default': 'sqlite',
|
|
14
|
+
'database.connections.sqlite': { driver: 'sqlite', database: ':memory:' },
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// 模擬 Launchpad 的載入機制
|
|
19
|
+
console.log('🛰️ [Launchpad] 正在將衛星對接至核心引掣...')
|
|
20
|
+
await core.use(new CommerceServiceProvider())
|
|
21
|
+
await core.bootstrap()
|
|
22
|
+
|
|
23
|
+
// 驗證 1: 服務是否成功註冊
|
|
24
|
+
const hasPlaceOrder = core.container.has('commerce.place-order')
|
|
25
|
+
console.log(`✅ [Launchpad] 服務對接: ${hasPlaceOrder ? '成功' : '失敗'}`)
|
|
26
|
+
|
|
27
|
+
// 驗證 2: 環境感應
|
|
28
|
+
// 我們透過執行一次下單來確認它是否進入了 Sport 模式
|
|
29
|
+
const _placeOrder = core.container.make('commerce.place-order') as any
|
|
30
|
+
// 這裡我們不跑完整邏輯,只檢查內部配置感應
|
|
31
|
+
console.log(`✅ [Launchpad] 模式感應: ${process.env.COMMERCE_MODE} 模式運作中`)
|
|
32
|
+
|
|
33
|
+
console.log('\n🌟 [Launchpad] 衛星 "commerce" 已成功通過點火驗證,具備發射條件!')
|
|
34
|
+
process.exit(0)
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('💥 [Launchpad] 點火失敗:', err)
|
|
37
|
+
process.exit(1)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
launchpadIgnition()
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
"tests/**/*"
|
|
21
|
+
],
|
|
22
|
+
"exclude": [
|
|
23
|
+
"dist",
|
|
24
|
+
"node_modules",
|
|
25
|
+
"**/*.test.ts"
|
|
26
|
+
]
|
|
27
|
+
}
|