Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ jobs:
chmod +x osv-scanner
(./osv-scanner --lockfile=pnpm-lock.yaml > .ci-status/osv.log 2>&1 || echo "1" > .ci-status/osv.fail) &

# 7. Knip Dependency & Export Analyzer
(pnpm lint:knip > .ci-status/knip.log 2>&1 || echo "1" > .ci-status/knip.fail) &
# # 7. Knip Dependency & Export Analyzer
# (pnpm lint:knip > .ci-status/knip.log 2>&1 || echo "1" > .ci-status/knip.fail) &

echo "All checks started. Waiting for completion..."
wait
Expand Down
1 change: 1 addition & 0 deletions .markdownlint.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"first-line-heading": false,
"line-length": false,
"no-emphasis-as-header": false,
"no-duplicate-heading": { "siblings_only": true },
"no-inline-html": false
}
File renamed without changes.
File renamed without changes.
18 changes: 18 additions & 0 deletions bruno/billing/get-payment-by-id.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
meta {
name: Get Payment By ID
type: http
seq: 4
}

get {
url: {{baseUrl}}/{{apiPrefix}}/v1/billing/payments/{{paymentId}}
auth: bearer
}

auth:bearer {
token: {{accessToken}}
}

headers {
x-tenant-id: {{tenantId}}
}
23 changes: 23 additions & 0 deletions bruno/billing/list-payments.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
meta {
name: List Payments
type: http
seq: 3
}

get {
url: {{baseUrl}}/{{apiPrefix}}/v1/billing/payments?page=1&limit=20
auth: bearer
}

auth:bearer {
token: {{accessToken}}
}

headers {
x-tenant-id: {{tenantId}}
}

params:query {
page: 1
limit: 20
}
20 changes: 20 additions & 0 deletions bruno/billing/simulate-payment-failure.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
meta {
name: Simulate Payment Failure
type: http
seq: 2
}

post {
url: {{baseUrl}}/{{apiPrefix}}/v1/webhooks/fake
body: json
}

body:json {
{
"type": "payment.failed",
"providerPaymentId": "fake_pi_test_fail_001",
"amount": 9900,
"currency": "usd",
"failureReason": "Insufficient funds"
}
}
19 changes: 19 additions & 0 deletions bruno/billing/simulate-payment-success.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
meta {
name: Simulate Payment Success
type: http
seq: 1
}

post {
url: {{baseUrl}}/{{apiPrefix}}/v1/webhooks/fake
body: json
}

body:json {
{
"type": "payment.succeeded",
"providerPaymentId": "fake_pi_test_success_001",
"amount": 9900,
"currency": "usd"
}
}
1 change: 1 addition & 0 deletions bruno/environments/local.bru
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ vars {
subscriptionId:
apiKey:
invoiceId:
paymentId:
}
183 changes: 183 additions & 0 deletions docs/api/billing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Billing API

The Billing API provides access to ledger entries, balance, and payment history. All endpoints are tenant-scoped — they require a valid JWT and `x-tenant-id` header.

---

## Get Billing Ledger

Returns paginated ledger entries for the tenant, newest first.

**Endpoint:** `GET /api/v1/billing/ledger`

### Query Parameters

| Parameter | Type | Default | Description |
| :-------- | :----- | :------ | :---------------------------------------------------------- |
| `page` | number | 1 | Page number |
| `limit` | number | 20 | Results per page (max 100) |
| `type` | string | — | Filter by type: CHARGE, PAYMENT, CREDIT, REFUND, ADJUSTMENT |

### Response

```json
{
"data": [
{
"id": "uuid",
"tenantId": "uuid",
"invoiceId": "uuid",
"type": "CHARGE",
"description": "Invoice INV-2026-0001 finalized",
"debit": 9900,
"credit": 0,
"currency": "usd",
"externalReference": null,
"createdAt": "2026-05-22T10:00:00Z"
}
],
"meta": { "total": 6, "page": 1, "limit": 20, "totalPages": 1 }
}
```

---

## Get Balance

Returns the tenant's current billing balance.

**Endpoint:** `GET /api/v1/billing/balance`

### Response

```json
{
"tenantId": "uuid",
"currency": "usd",
"balance": 0,
"totalCharged": 9900,
"totalPaid": 9900,
"unit": "cents",
"asOf": "2026-05-22T10:00:00Z"
}
```

Balance = SUM(debit) - SUM(credit). Positive means the tenant owes money. Zero means fully paid.

---

## List Payment Attempts

Returns paginated payment attempts for the tenant, newest first.

**Endpoint:** `GET /api/v1/billing/payments`

### Query Parameters

| Parameter | Type | Default | Description |
| :-------- | :----- | :------ | :------------------------------------------------------------- |
| `page` | number | 1 | Page number |
| `limit` | number | 20 | Results per page (max 100) |
| `status` | string | — | Filter: PENDING, SUCCEEDED, FAILED, CANCELLED, REQUIRES_ACTION |

### Response

```json
{
"data": [
{
"id": "uuid",
"invoiceId": "uuid",
"invoiceNumber": "INV-2026-0001",
"tenantId": "uuid",
"providerPaymentId": "fake_pi_seed_inv20260001",
"provider": "fake",
"status": "SUCCEEDED",
"amount": 9900,
"currency": "usd",
"failureReason": null,
"attemptNumber": 0,
"nextRetryAt": null,
"createdAt": "2026-05-22T10:00:00Z",
"updatedAt": "2026-05-22T10:00:00Z"
}
],
"meta": { "total": 1, "page": 1, "limit": 20, "totalPages": 1 }
}
```

---

## Get Payment Attempt Details

Returns a single payment attempt with full provider response.

**Endpoint:** `GET /api/v1/billing/payments/:id`

### Path Parameters

| Parameter | Type | Description |
| :-------- | :--- | :------------------- |
| `id` | UUID | Payment attempt UUID |

### Response

```json
{
"id": "uuid",
"invoiceId": "uuid",
"invoiceNumber": "INV-2026-0001",
"tenantId": "uuid",
"providerPaymentId": "fake_pi_seed_inv20260001",
"provider": "fake",
"status": "SUCCEEDED",
"amount": 9900,
"currency": "usd",
"failureReason": null,
"attemptNumber": 0,
"nextRetryAt": null,
"providerResponse": {
"source": "seed",
"status": "succeeded"
},
"createdAt": "2026-05-22T10:00:00Z",
"updatedAt": "2026-05-22T10:00:00Z"
}
```

### Error Responses

| Status | Description |
| :----- | :------------------------ |
| 404 | Payment attempt not found |

---

## Webhook Endpoints

These endpoints receive events from payment providers. They have NO authentication guard — the provider's signature is the authentication.

### Stripe Webhook

**Endpoint:** `POST /api/v1/webhooks/stripe`

Requires `stripe-signature` header. Returns 200 immediately; processes asynchronously.

### Fake Webhook (dev/test only)

**Endpoint:** `POST /api/v1/webhooks/fake`

Accepts JSON body directly. No signature validation. Use for testing payment flows locally.

### Simulating a payment webhook

```bash
curl -X POST http://localhost:3000/api/v1/webhooks/fake \
-H "Content-Type: application/json" \
-d '{
"type": "payment.succeeded",
"providerPaymentId": "fake_pi_test_001",
"amount": 9900,
"currency": "usd"
}'
```
Loading
Loading