@gravito/satellite-payment 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 ADDED
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ dist
3
+ .git
4
+ .env
5
+ *.log
6
+ .vscode
7
+ .idea
8
+ tests
package/.env.example ADDED
@@ -0,0 +1,19 @@
1
+ # Application
2
+ APP_NAME="payment"
3
+ APP_ENV=development
4
+ APP_KEY=
5
+ APP_DEBUG=true
6
+ APP_URL=http://localhost:3000
7
+
8
+ # Server
9
+ PORT=3000
10
+
11
+ # Database
12
+ DB_CONNECTION=sqlite
13
+ DB_DATABASE=database/database.sqlite
14
+
15
+ # Cache
16
+ CACHE_DRIVER=memory
17
+
18
+ # Logging
19
+ LOG_LEVEL=debug
@@ -0,0 +1,14 @@
1
+ # payment Satellite Architecture
2
+
3
+ This satellite follows the Gravito Satellite Specification v1.0.
4
+
5
+ ## Design
6
+ - **DDD**: Domain logic is separated from framework concerns.
7
+ - **Dogfooding**: Uses official Gravito modules (@gravito/atlas, @gravito/stasis).
8
+ - **Decoupled**: Inter-satellite communication happens via Contracts and Events.
9
+
10
+ ## Layers
11
+ - **Domain**: Pure business rules.
12
+ - **Application**: Orchestration of domain tasks.
13
+ - **Infrastructure**: Implementation of persistence and external services.
14
+ - **Interface**: HTTP and Event entry points.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # @gravito/satellite-payment
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
11
+ - @gravito/stasis@1.0.0
package/Dockerfile ADDED
@@ -0,0 +1,25 @@
1
+ FROM oven/bun:1.0 AS base
2
+ WORKDIR /usr/src/app
3
+
4
+ # Install dependencies
5
+ FROM base AS install
6
+ RUN mkdir -p /temp/dev
7
+ COPY package.json bun.lockb /temp/dev/
8
+ RUN cd /temp/dev && bun install --frozen-lockfile
9
+
10
+ # Build application
11
+ FROM base AS build
12
+ COPY --from=install /temp/dev/node_modules node_modules
13
+ COPY . .
14
+ ENV NODE_ENV=production
15
+ RUN bun run build
16
+
17
+ # Final production image
18
+ FROM base AS release
19
+ COPY --from=build /usr/src/app/dist/bootstrap.js index.js
20
+ COPY --from=build /usr/src/app/package.json .
21
+
22
+ # Create a non-root user for security
23
+ USER bun
24
+ EXPOSE 3000/tcp
25
+ ENTRYPOINT [ "bun", "run", "index.js" ]
package/WHITEPAPER.md ADDED
@@ -0,0 +1,43 @@
1
+ # Gravito Payment Satellite - Technical Whitepaper
2
+
3
+ ## Overview
4
+ The Payment Satellite is the financial clearing house of the Gravito ecosystem. It provides a standardized, asynchronous, and secure way to handle monetary transactions using the **GASS (Gravito Asynchronous Satellite Specification)**.
5
+
6
+ ## Core Concepts
7
+
8
+ ### 1. Transaction Ledger
9
+ Unlike simple payment callbacks, the Payment satellite maintains an immutable ledger of transactions. Every payment intent, authorization, and capture is logged as a state transition in the `Transaction` entity.
10
+
11
+ ### 2. Dual-Phase Clearing (Authorize & Capture)
12
+ To support high-trust commerce, the satellite supports a two-step clearing process:
13
+ - **Authorize**: Reserve funds on the user's payment method (triggered by order creation).
14
+ - **Capture**: Finalize the transfer (triggered by shipping or digital delivery).
15
+
16
+ ## Propulsion Stages
17
+
18
+ ### Stage 1: Standard (SQL)
19
+ - Records all ledger entries in relational tables.
20
+ - Ensures strict ACID compliance for financial auditing.
21
+
22
+ ### Stage 2: Sport (In-Memory)
23
+ - **Intent Pre-warming**: When a user enters the checkout page, the satellite pre-computes the PaymentIntent and caches it in memory.
24
+ - Reduces checkout latency by ~200ms.
25
+
26
+ ### Stage 3: Turbo (Asynchronous Webhooks)
27
+ - Utilizes `gravito-echo` to handle high-frequency webhooks from Stripe/PayPal.
28
+ - Processes clearing and fraud checks in background queues to keep the main thread responsive.
29
+
30
+ ## Event Driven Architecture
31
+
32
+ ### Outbound Events
33
+ - `payment:intent:ready`: Notifies Commerce that the payment session is ready for the UI.
34
+ - `payment:succeeded`: Notifies Commerce to mark the order as `PAID`.
35
+ - `payment:refund:succeeded`: Notifies Commerce to mark the order as `REFUNDED`.
36
+
37
+ ### Inbound Actions
38
+ - `commerce:order:created`: Automatically initiates the payment flow.
39
+ - `commerce:order:refund-requested`: Automatically initiates the refund gateway call.
40
+
41
+ ## Security
42
+ - **Strict Isolation**: No raw card data ever touches the Gravito core. All sensitive data is handled via Gateway Tokens (Stripe Tokens/Secrets).
43
+ - **Idempotency**: All `pay()` and `refund()` calls require an idempotency key to prevent double-charging.
@@ -0,0 +1,8 @@
1
+ import { ServiceProvider, Container } from '@gravito/core';
2
+
3
+ declare class PaymentServiceProvider extends ServiceProvider {
4
+ register(container: Container): void;
5
+ boot(): void;
6
+ }
7
+
8
+ export { PaymentServiceProvider };
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ // src/index.ts
2
+ import { ServiceProvider } from "@gravito/core";
3
+
4
+ // src/Application/UseCases/ProcessPayment.ts
5
+ import { UseCase } from "@gravito/enterprise";
6
+
7
+ // src/Domain/Entities/Transaction.ts
8
+ import { Entity } from "@gravito/enterprise";
9
+ var Transaction = class _Transaction extends Entity {
10
+ constructor(id, props) {
11
+ super(id);
12
+ this.props = props;
13
+ }
14
+ static create(id, props) {
15
+ return new _Transaction(id, {
16
+ ...props,
17
+ status: "pending" /* PENDING */,
18
+ metadata: {},
19
+ createdAt: /* @__PURE__ */ new Date()
20
+ });
21
+ }
22
+ authorize(gatewayId) {
23
+ if (this.props.status !== "pending" /* PENDING */) {
24
+ throw new Error("Only pending transactions can be authorized");
25
+ }
26
+ this.props.status = "authorized" /* AUTHORIZED */;
27
+ this.props.gatewayTransactionId = gatewayId;
28
+ }
29
+ capture() {
30
+ if (this.props.status !== "authorized" /* AUTHORIZED */) {
31
+ throw new Error("Only authorized transactions can be captured");
32
+ }
33
+ this.props.status = "captured" /* CAPTURED */;
34
+ }
35
+ refund() {
36
+ if (this.props.status !== "captured" /* CAPTURED */) {
37
+ throw new Error("Only captured transactions can be refunded");
38
+ }
39
+ this.props.status = "refunded" /* REFUNDED */;
40
+ }
41
+ fail(reason) {
42
+ this.props.status = "failed" /* FAILED */;
43
+ this.props.metadata.failReason = reason;
44
+ }
45
+ get orderId() {
46
+ return this.props.orderId;
47
+ }
48
+ get amount() {
49
+ return this.props.amount;
50
+ }
51
+ get status() {
52
+ return this.props.status;
53
+ }
54
+ get gateway() {
55
+ return this.props.gateway;
56
+ }
57
+ };
58
+
59
+ // src/Application/UseCases/ProcessPayment.ts
60
+ var ProcessPayment = class extends UseCase {
61
+ constructor(manager) {
62
+ super();
63
+ this.manager = manager;
64
+ }
65
+ async execute(input) {
66
+ const transaction = Transaction.create(crypto.randomUUID(), {
67
+ orderId: input.orderId,
68
+ amount: input.amount,
69
+ currency: input.currency,
70
+ gateway: input.gateway || "default"
71
+ });
72
+ const gateway = this.manager.driver(input.gateway);
73
+ const intent = await gateway.createIntent(transaction);
74
+ transaction.authorize(intent.gatewayTransactionId);
75
+ return intent;
76
+ }
77
+ };
78
+
79
+ // src/Application/UseCases/RefundPayment.ts
80
+ import { UseCase as UseCase2 } from "@gravito/enterprise";
81
+ var RefundPayment = class extends UseCase2 {
82
+ constructor(gateway) {
83
+ super();
84
+ this.gateway = gateway;
85
+ }
86
+ async execute(input) {
87
+ console.log(`[Payment] Processing refund for transaction: ${input.gatewayTransactionId}`);
88
+ return await this.gateway.refund(input.gatewayTransactionId, input.amount);
89
+ }
90
+ };
91
+
92
+ // src/Infrastructure/Gateways/StripeGateway.ts
93
+ import Stripe from "stripe";
94
+ var StripeGateway = class {
95
+ stripe;
96
+ constructor(apiKey) {
97
+ this.stripe = new Stripe(apiKey, {
98
+ apiVersion: "2025-01-27"
99
+ });
100
+ }
101
+ getName() {
102
+ return "stripe";
103
+ }
104
+ async createIntent(transaction) {
105
+ const rawProps = transaction.props;
106
+ const intent = await this.stripe.paymentIntents.create({
107
+ amount: Math.round(transaction.amount * 100),
108
+ currency: rawProps.currency.toLowerCase(),
109
+ metadata: {
110
+ orderId: transaction.orderId,
111
+ transactionId: transaction.id
112
+ },
113
+ automatic_payment_methods: {
114
+ enabled: true
115
+ }
116
+ });
117
+ return {
118
+ gatewayTransactionId: intent.id,
119
+ clientSecret: intent.client_secret || "",
120
+ status: intent.status
121
+ };
122
+ }
123
+ async capture(gatewayTransactionId) {
124
+ const intent = await this.stripe.paymentIntents.capture(gatewayTransactionId);
125
+ return intent.status === "succeeded";
126
+ }
127
+ async refund(gatewayTransactionId, amount) {
128
+ const refund = await this.stripe.refunds.create({
129
+ payment_intent: gatewayTransactionId,
130
+ ...amount && { amount: Math.round(amount * 100) }
131
+ });
132
+ return refund.status === "succeeded";
133
+ }
134
+ };
135
+
136
+ // src/Infrastructure/PaymentManager.ts
137
+ var PaymentManager = class {
138
+ constructor(core) {
139
+ this.core = core;
140
+ }
141
+ drivers = /* @__PURE__ */ new Map();
142
+ /**
143
+ * 註冊金流驅動器 (這就是日後掛載 ECPay, LINE Pay 的入口)
144
+ */
145
+ extend(name, resolver) {
146
+ this.core.logger.info(`[PaymentManager] Driver registered: ${name}`);
147
+ this.drivers.set(name, resolver);
148
+ }
149
+ /**
150
+ * 取得指定或預設的驅動器
151
+ */
152
+ driver(name) {
153
+ const driverName = name || this.core.config.get("payment.default", "stripe");
154
+ const resolver = this.drivers.get(driverName);
155
+ if (!resolver) {
156
+ throw new Error(
157
+ `Payment driver [${driverName}] is not registered. Did you forget to mount the gateway satellite?`
158
+ );
159
+ }
160
+ return resolver();
161
+ }
162
+ };
163
+
164
+ // src/Interface/Http/Controllers/StripeWebhookController.ts
165
+ import Stripe2 from "stripe";
166
+ var StripeWebhookController = class {
167
+ async handle(c) {
168
+ const core = c.get("core");
169
+ const stripeKey = core.config.get("payment.stripe.secret") || "";
170
+ const webhookSecret = core.config.get("payment.stripe.webhook_secret") || "";
171
+ const stripe = new Stripe2(stripeKey);
172
+ const signature = c.req.header("stripe-signature") || "";
173
+ try {
174
+ const body = await c.req.text();
175
+ const event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
176
+ if (event.type === "payment_intent.succeeded") {
177
+ const intent = event.data.object;
178
+ core.logger.info(`[Payment] Webhook: Payment succeeded for ${intent.id}`);
179
+ await core.hooks.doAction("payment:succeeded", {
180
+ gatewayTransactionId: intent.id,
181
+ orderId: intent.metadata.orderId,
182
+ amount: intent.amount / 100
183
+ });
184
+ }
185
+ return c.json({ received: true });
186
+ } catch (err) {
187
+ core.logger.error(`[Payment] Webhook Error: ${err.message}`);
188
+ return c.json({ error: "Webhook signature verification failed" }, 400);
189
+ }
190
+ }
191
+ };
192
+
193
+ // src/index.ts
194
+ var PaymentServiceProvider = class extends ServiceProvider {
195
+ register(container) {
196
+ container.singleton("payment.manager", (_c) => new PaymentManager(this.core));
197
+ container.singleton("payment.process", (c) => {
198
+ return new ProcessPayment(c.make("payment.manager"));
199
+ });
200
+ container.singleton("payment.refund", (c) => {
201
+ return new RefundPayment(c.make("payment.manager"));
202
+ });
203
+ }
204
+ boot() {
205
+ const core = this.core;
206
+ if (!core) {
207
+ return;
208
+ }
209
+ const manager = core.container.make("payment.manager");
210
+ manager.extend("stripe", () => {
211
+ const apiKey = core.config.get("payment.stripe.secret") || "mock_key";
212
+ return new StripeGateway(apiKey);
213
+ });
214
+ const webhookCtrl = new StripeWebhookController();
215
+ core.router.post("/webhooks/payment/stripe", (c) => webhookCtrl.handle(c));
216
+ core.logger.info("\u{1F6F0}\uFE0F Satellite Payment is operational (Manager Ready)");
217
+ core.hooks.addAction("commerce:order:created", async (payload) => {
218
+ const processPayment = core.container.make("payment.process");
219
+ try {
220
+ const intent = await processPayment.execute({
221
+ orderId: payload.order.id,
222
+ amount: payload.order.totalAmount,
223
+ currency: payload.order.currency
224
+ // gateway: 'stripe' // 可以由配置或 Payload 決定
225
+ });
226
+ await core.hooks.doAction("payment:intent:ready", { orderId: payload.order.id, intent });
227
+ } catch (error) {
228
+ core.logger.error(`[Payment] Process error: ${error.message}`);
229
+ }
230
+ });
231
+ core.hooks.addAction("payment:succeeded", async (payload) => {
232
+ core.logger.info(`[Payment] Order confirmed as PAID: ${payload.orderId}`);
233
+ });
234
+ }
235
+ };
236
+ export {
237
+ PaymentServiceProvider
238
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@gravito/satellite-payment",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsup src/index.ts --format esm --dts --clean --external @gravito/atlas --external @gravito/enterprise --external @gravito/stasis --external @gravito/core --external stripe",
10
+ "test": "bun test",
11
+ "typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck"
12
+ },
13
+ "dependencies": {
14
+ "@gravito/atlas": "workspace:*",
15
+ "@gravito/enterprise": "workspace:*",
16
+ "@gravito/stasis": "workspace:*",
17
+ "@gravito/core": "workspace:*",
18
+ "stripe": "^20.1.0"
19
+ },
20
+ "devDependencies": {
21
+ "tsup": "^8.0.0",
22
+ "typescript": "^5.9.3"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/gravito-framework/gravito.git",
30
+ "directory": "satellites/payment"
31
+ }
32
+ }
@@ -0,0 +1,22 @@
1
+ import { UseCase } from '@gravito/enterprise'
2
+ import type { IPaymentRepository } from '../../Domain/Contracts/IPaymentRepository'
3
+ import { Payment } from '../../Domain/Entities/Payment'
4
+
5
+ export interface CreatePaymentInput {
6
+ name: string
7
+ }
8
+
9
+ export class CreatePayment extends UseCase<CreatePaymentInput, string> {
10
+ constructor(private repository: IPaymentRepository) {
11
+ super()
12
+ }
13
+
14
+ async execute(input: CreatePaymentInput): Promise<string> {
15
+ const id = crypto.randomUUID()
16
+ const entity = Payment.create(id, input.name)
17
+
18
+ await this.repository.save(entity)
19
+
20
+ return id
21
+ }
22
+ }
@@ -0,0 +1,34 @@
1
+ import { UseCase } from '@gravito/enterprise'
2
+ import type { PaymentIntent } from '../../Domain/Contracts/IPaymentGateway'
3
+ import { Transaction } from '../../Domain/Entities/Transaction'
4
+ import type { PaymentManager } from '../../Infrastructure/PaymentManager'
5
+
6
+ export interface ProcessPaymentInput {
7
+ orderId: string
8
+ amount: number
9
+ currency: string
10
+ gateway?: string // 現在可以動態指定
11
+ }
12
+
13
+ export class ProcessPayment extends UseCase<ProcessPaymentInput, PaymentIntent> {
14
+ constructor(private manager: PaymentManager) {
15
+ super()
16
+ }
17
+
18
+ async execute(input: ProcessPaymentInput): Promise<PaymentIntent> {
19
+ const transaction = Transaction.create(crypto.randomUUID(), {
20
+ orderId: input.orderId,
21
+ amount: input.amount,
22
+ currency: input.currency,
23
+ gateway: input.gateway || 'default',
24
+ })
25
+
26
+ // 從 Manager 取得指定的金流驅動器
27
+ const gateway = this.manager.driver(input.gateway)
28
+ const intent = await gateway.createIntent(transaction)
29
+
30
+ transaction.authorize(intent.gatewayTransactionId)
31
+
32
+ return intent
33
+ }
34
+ }
@@ -0,0 +1,18 @@
1
+ import { UseCase } from '@gravito/enterprise'
2
+ import type { IPaymentGateway } from '../../Domain/Contracts/IPaymentGateway'
3
+
4
+ export interface RefundPaymentInput {
5
+ gatewayTransactionId: string
6
+ amount?: number
7
+ }
8
+
9
+ export class RefundPayment extends UseCase<RefundPaymentInput, boolean> {
10
+ constructor(private gateway: IPaymentGateway) {
11
+ super()
12
+ }
13
+
14
+ async execute(input: RefundPaymentInput): Promise<boolean> {
15
+ console.log(`[Payment] Processing refund for transaction: ${input.gatewayTransactionId}`)
16
+ return await this.gateway.refund(input.gatewayTransactionId, input.amount)
17
+ }
18
+ }
@@ -0,0 +1,26 @@
1
+ import type { Transaction } from '../Entities/Transaction'
2
+
3
+ export interface PaymentIntent {
4
+ gatewayTransactionId: string
5
+ clientSecret: string // 用於前端付款(如 Stripe Elements)
6
+ status: string
7
+ }
8
+
9
+ export interface IPaymentGateway {
10
+ getName(): string
11
+
12
+ /**
13
+ * 建立支付意向 (Create Intent)
14
+ */
15
+ createIntent(transaction: Transaction): Promise<PaymentIntent>
16
+
17
+ /**
18
+ * 清算授權金額 (Capture)
19
+ */
20
+ capture(gatewayTransactionId: string): Promise<boolean>
21
+
22
+ /**
23
+ * 退款 (Refund)
24
+ */
25
+ refund(gatewayTransactionId: string, amount?: number): Promise<boolean>
26
+ }
@@ -0,0 +1,6 @@
1
+ import type { Repository } from '@gravito/enterprise'
2
+ import type { Payment } from '../Entities/Payment'
3
+
4
+ export interface IPaymentRepository extends Repository<Payment, string> {
5
+ // Add custom methods here
6
+ }
@@ -0,0 +1,26 @@
1
+ import { Entity } from '@gravito/enterprise'
2
+
3
+ export interface PaymentProps {
4
+ name: string
5
+ createdAt: Date
6
+ }
7
+
8
+ export class Payment extends Entity<string> {
9
+ constructor(
10
+ id: string,
11
+ private props: PaymentProps
12
+ ) {
13
+ super(id)
14
+ }
15
+
16
+ static create(id: string, name: string): Payment {
17
+ return new Payment(id, {
18
+ name,
19
+ createdAt: new Date(),
20
+ })
21
+ }
22
+
23
+ get name() {
24
+ return this.props.name
25
+ }
26
+ }
@@ -0,0 +1,81 @@
1
+ import { Entity } from '@gravito/enterprise'
2
+
3
+ export enum TransactionStatus {
4
+ PENDING = 'pending',
5
+ AUTHORIZED = 'authorized',
6
+ CAPTURED = 'captured',
7
+ FAILED = 'failed',
8
+ REFUNDED = 'refunded',
9
+ }
10
+
11
+ export interface TransactionProps {
12
+ orderId: string
13
+ amount: number
14
+ currency: string
15
+ gateway: string
16
+ gatewayTransactionId?: string
17
+ status: TransactionStatus
18
+ metadata: Record<string, any>
19
+ createdAt: Date
20
+ }
21
+
22
+ export class Transaction extends Entity<string> {
23
+ constructor(
24
+ id: string,
25
+ private props: TransactionProps
26
+ ) {
27
+ super(id)
28
+ }
29
+
30
+ static create(
31
+ id: string,
32
+ props: Omit<TransactionProps, 'status' | 'createdAt' | 'metadata'>
33
+ ): Transaction {
34
+ return new Transaction(id, {
35
+ ...props,
36
+ status: TransactionStatus.PENDING,
37
+ metadata: {},
38
+ createdAt: new Date(),
39
+ })
40
+ }
41
+
42
+ authorize(gatewayId: string): void {
43
+ if (this.props.status !== TransactionStatus.PENDING) {
44
+ throw new Error('Only pending transactions can be authorized')
45
+ }
46
+ this.props.status = TransactionStatus.AUTHORIZED
47
+ this.props.gatewayTransactionId = gatewayId
48
+ }
49
+
50
+ capture(): void {
51
+ if (this.props.status !== TransactionStatus.AUTHORIZED) {
52
+ throw new Error('Only authorized transactions can be captured')
53
+ }
54
+ this.props.status = TransactionStatus.CAPTURED
55
+ }
56
+
57
+ refund(): void {
58
+ if (this.props.status !== TransactionStatus.CAPTURED) {
59
+ throw new Error('Only captured transactions can be refunded')
60
+ }
61
+ this.props.status = TransactionStatus.REFUNDED
62
+ }
63
+
64
+ fail(reason: string): void {
65
+ this.props.status = TransactionStatus.FAILED
66
+ this.props.metadata.failReason = reason
67
+ }
68
+
69
+ get orderId() {
70
+ return this.props.orderId
71
+ }
72
+ get amount() {
73
+ return this.props.amount
74
+ }
75
+ get status() {
76
+ return this.props.status
77
+ }
78
+ get gateway() {
79
+ return this.props.gateway
80
+ }
81
+ }
@@ -0,0 +1,53 @@
1
+ import Stripe from 'stripe'
2
+ import type { IPaymentGateway, PaymentIntent } from '../../Domain/Contracts/IPaymentGateway'
3
+ import type { Transaction } from '../../Domain/Entities/Transaction'
4
+
5
+ export class StripeGateway implements IPaymentGateway {
6
+ private stripe: Stripe
7
+
8
+ constructor(apiKey: string) {
9
+ this.stripe = new Stripe(apiKey, {
10
+ apiVersion: '2025-01-27' as any,
11
+ })
12
+ }
13
+
14
+ getName(): string {
15
+ return 'stripe'
16
+ }
17
+
18
+ async createIntent(transaction: Transaction): Promise<PaymentIntent> {
19
+ // 透過 any 訪問私有屬性以解決跨包訪問問題
20
+ const rawProps = (transaction as any).props
21
+
22
+ const intent = await this.stripe.paymentIntents.create({
23
+ amount: Math.round(transaction.amount * 100),
24
+ currency: rawProps.currency.toLowerCase(),
25
+ metadata: {
26
+ orderId: transaction.orderId,
27
+ transactionId: transaction.id,
28
+ },
29
+ automatic_payment_methods: {
30
+ enabled: true,
31
+ },
32
+ })
33
+
34
+ return {
35
+ gatewayTransactionId: intent.id,
36
+ clientSecret: intent.client_secret || '',
37
+ status: intent.status,
38
+ }
39
+ }
40
+
41
+ async capture(gatewayTransactionId: string): Promise<boolean> {
42
+ const intent = await this.stripe.paymentIntents.capture(gatewayTransactionId)
43
+ return intent.status === 'succeeded'
44
+ }
45
+
46
+ async refund(gatewayTransactionId: string, amount?: number): Promise<boolean> {
47
+ const refund = await this.stripe.refunds.create({
48
+ payment_intent: gatewayTransactionId,
49
+ ...(amount && { amount: Math.round(amount * 100) }),
50
+ })
51
+ return refund.status === 'succeeded'
52
+ }
53
+ }
@@ -0,0 +1,32 @@
1
+ import type { PlanetCore } from '@gravito/core'
2
+ import type { IPaymentGateway } from '../Domain/Contracts/IPaymentGateway'
3
+
4
+ export class PaymentManager {
5
+ private drivers = new Map<string, () => IPaymentGateway>()
6
+
7
+ constructor(private core: PlanetCore) {}
8
+
9
+ /**
10
+ * 註冊金流驅動器 (這就是日後掛載 ECPay, LINE Pay 的入口)
11
+ */
12
+ extend(name: string, resolver: () => IPaymentGateway): void {
13
+ this.core.logger.info(`[PaymentManager] Driver registered: ${name}`)
14
+ this.drivers.set(name, resolver)
15
+ }
16
+
17
+ /**
18
+ * 取得指定或預設的驅動器
19
+ */
20
+ driver(name?: string): IPaymentGateway {
21
+ const driverName = name || this.core.config.get<string>('payment.default', 'stripe')
22
+ const resolver = this.drivers.get(driverName)
23
+
24
+ if (!resolver) {
25
+ throw new Error(
26
+ `Payment driver [${driverName}] is not registered. Did you forget to mount the gateway satellite?`
27
+ )
28
+ }
29
+
30
+ return resolver()
31
+ }
32
+ }
@@ -0,0 +1,24 @@
1
+ import type { IPaymentRepository } from '../../Domain/Contracts/IPaymentRepository'
2
+ import type { Payment } from '../../Domain/Entities/Payment'
3
+
4
+ export class AtlasPaymentRepository implements IPaymentRepository {
5
+ async save(entity: Payment): Promise<void> {
6
+ // Dogfooding: Use @gravito/atlas for persistence
7
+ console.log('[Atlas] Saving entity:', entity.id)
8
+ // await DB.table('payments').insert({ ... })
9
+ }
10
+
11
+ async findById(_id: string): Promise<Payment | null> {
12
+ return null
13
+ }
14
+
15
+ async findAll(): Promise<Payment[]> {
16
+ return []
17
+ }
18
+
19
+ async delete(_id: string): Promise<void> {}
20
+
21
+ async exists(_id: string): Promise<boolean> {
22
+ return false
23
+ }
24
+ }
@@ -0,0 +1,34 @@
1
+ import type { GravitoContext, PlanetCore } from '@gravito/core'
2
+ import Stripe from 'stripe'
3
+
4
+ export class StripeWebhookController {
5
+ async handle(c: GravitoContext) {
6
+ const core = c.get('core') as PlanetCore
7
+ const stripeKey = core.config.get<string>('payment.stripe.secret') || ''
8
+ const webhookSecret = core.config.get<string>('payment.stripe.webhook_secret') || ''
9
+
10
+ const stripe = new Stripe(stripeKey)
11
+ const signature = c.req.header('stripe-signature') || ''
12
+
13
+ try {
14
+ const body = await c.req.text()
15
+ const event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
16
+
17
+ if (event.type === 'payment_intent.succeeded') {
18
+ const intent = event.data.object as Stripe.PaymentIntent
19
+ core.logger.info(`[Payment] Webhook: Payment succeeded for ${intent.id}`)
20
+
21
+ await core.hooks.doAction('payment:succeeded', {
22
+ gatewayTransactionId: intent.id,
23
+ orderId: intent.metadata.orderId,
24
+ amount: intent.amount / 100,
25
+ })
26
+ }
27
+
28
+ return c.json({ received: true })
29
+ } catch (err: any) {
30
+ core.logger.error(`[Payment] Webhook Error: ${err.message}`)
31
+ return c.json({ error: 'Webhook signature verification failed' }, 400)
32
+ }
33
+ }
34
+ }
package/src/env.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ interface ImportMeta {
2
+ readonly dir: string
3
+ readonly path: string
4
+ }
package/src/index.ts ADDED
@@ -0,0 +1,66 @@
1
+ import type { Container, GravitoContext } from '@gravito/core'
2
+ import { ServiceProvider } from '@gravito/core'
3
+ import { ProcessPayment } from './Application/UseCases/ProcessPayment'
4
+ import { RefundPayment } from './Application/UseCases/RefundPayment'
5
+ import { StripeGateway } from './Infrastructure/Gateways/StripeGateway'
6
+ import { PaymentManager } from './Infrastructure/PaymentManager'
7
+ import { StripeWebhookController } from './Interface/Http/Controllers/StripeWebhookController'
8
+
9
+ export class PaymentServiceProvider extends ServiceProvider {
10
+ register(container: Container): void {
11
+ // 1. 綁定管理器
12
+ container.singleton('payment.manager', (_c) => new PaymentManager(this.core!))
13
+
14
+ // 2. 綁定 UseCases (傳入 Manager 而非具體 Gateway)
15
+ container.singleton('payment.process', (c) => {
16
+ return new ProcessPayment(c.make('payment.manager'))
17
+ })
18
+
19
+ container.singleton('payment.refund', (c) => {
20
+ return new RefundPayment(c.make('payment.manager'))
21
+ })
22
+ }
23
+
24
+ override boot(): void {
25
+ const core = this.core
26
+ if (!core) {
27
+ return
28
+ }
29
+
30
+ const manager = core.container.make<PaymentManager>('payment.manager')
31
+
32
+ // 🌟 註冊 Stripe 驅動器 (這就是「掛載」動作)
33
+ // 雖然代碼在 Infrastructure 裡,但只有這裡被呼叫時才會建立實例
34
+ manager.extend('stripe', () => {
35
+ const apiKey = core.config.get<string>('payment.stripe.secret') || 'mock_key'
36
+ return new StripeGateway(apiKey)
37
+ })
38
+
39
+ const webhookCtrl = new StripeWebhookController()
40
+ core.router.post('/webhooks/payment/stripe', (c: GravitoContext) => webhookCtrl.handle(c))
41
+
42
+ core.logger.info('🛰️ Satellite Payment is operational (Manager Ready)')
43
+
44
+ /**
45
+ * GASS 聯動:監聽訂單建立
46
+ */
47
+ core.hooks.addAction('commerce:order:created', async (payload: any) => {
48
+ const processPayment = core.container.make<ProcessPayment>('payment.process')
49
+ try {
50
+ const intent = await processPayment.execute({
51
+ orderId: payload.order.id,
52
+ amount: payload.order.totalAmount,
53
+ currency: payload.order.currency,
54
+ // gateway: 'stripe' // 可以由配置或 Payload 決定
55
+ })
56
+ await core.hooks.doAction('payment:intent:ready', { orderId: payload.order.id, intent })
57
+ } catch (error: any) {
58
+ core.logger.error(`[Payment] Process error: ${error.message}`)
59
+ }
60
+ })
61
+
62
+ core.hooks.addAction('payment:succeeded', async (payload: any) => {
63
+ core.logger.info(`[Payment] Order confirmed as PAID: ${payload.orderId}`)
64
+ })
65
+ }
66
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "payment",
3
+ "id": "payment",
4
+ "version": "0.1.0",
5
+ "description": "A Gravito Satellite",
6
+ "capabilities": [
7
+ "create-payment"
8
+ ],
9
+ "requirements": [
10
+ "cache"
11
+ ],
12
+ "hooks": [
13
+ "payment:created"
14
+ ]
15
+ }
@@ -0,0 +1,7 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+
3
+ describe('Payment', () => {
4
+ it('should work', () => {
5
+ expect(true).toBe(true)
6
+ })
7
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
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
+ "exclude": [
22
+ "node_modules",
23
+ "dist",
24
+ "**/*.test.ts"
25
+ ]
26
+ }