@riligar/agents-kit 1.20.0 → 1.20.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/.agent/rules/naming-conventions.md +17 -17
- package/.agent/skills/riligar-dev-database/SKILL.md +15 -15
- package/.agent/skills/riligar-dev-database/references/connection.md +6 -8
- package/.agent/skills/riligar-dev-database/references/queries.md +25 -29
- package/.agent/skills/riligar-dev-database/references/schema.md +32 -18
- package/.agent/skills/riligar-dev-manager/SKILL.md +29 -29
- package/.agent/skills/riligar-infra-stripe/SKILL.md +46 -42
- package/.agent/skills/riligar-infra-stripe/references/stripe-database.md +62 -68
- package/package.json +1 -1
|
@@ -4,18 +4,18 @@ Consistent naming across all RiLiGar projects.
|
|
|
4
4
|
|
|
5
5
|
## Quick Reference
|
|
6
6
|
|
|
7
|
-
| Element
|
|
8
|
-
|
|
|
9
|
-
| Components
|
|
10
|
-
| Functions
|
|
11
|
-
| Variables
|
|
12
|
-
| Constants
|
|
13
|
-
| Files (components) | PascalCase
|
|
14
|
-
| Files (utilities)
|
|
15
|
-
| Directories
|
|
16
|
-
| CSS classes
|
|
17
|
-
|
|
|
18
|
-
| API endpoints
|
|
7
|
+
| Element | Convention | Example |
|
|
8
|
+
| ------------------ | --------------- | ---------------------------------------- |
|
|
9
|
+
| Components | PascalCase | `UserProfile`, `NavBar` |
|
|
10
|
+
| Functions | camelCase | `getUserData`, `handleSubmit` |
|
|
11
|
+
| Variables | camelCase | `userName`, `isLoading` |
|
|
12
|
+
| Constants | SCREAMING_SNAKE | `API_URL`, `MAX_RETRIES` |
|
|
13
|
+
| Files (components) | PascalCase | `UserProfile.jsx` |
|
|
14
|
+
| Files (utilities) | camelCase | `formatDate.js` |
|
|
15
|
+
| Directories | kebab-case | `user-profile/`, `api-utils/` |
|
|
16
|
+
| CSS classes | kebab-case | `nav-bar`, `user-card` |
|
|
17
|
+
| database tables | snake_case | `user_profiles`, `order_items` |
|
|
18
|
+
| API endpoints | kebab-case | `/api/user-profiles`, `/api/order-items` |
|
|
19
19
|
|
|
20
20
|
## Components
|
|
21
21
|
|
|
@@ -59,8 +59,8 @@ const MAX_RETRY_ATTEMPTS = 3
|
|
|
59
59
|
const DEFAULT_PAGE_SIZE = 20
|
|
60
60
|
|
|
61
61
|
// Bad
|
|
62
|
-
const apiBaseUrl = 'https://api.example.com'
|
|
63
|
-
const maxRetryAttempts = 3
|
|
62
|
+
const apiBaseUrl = 'https://api.example.com' // camelCase
|
|
63
|
+
const maxRetryAttempts = 3 // camelCase
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
## Booleans
|
|
@@ -131,10 +131,10 @@ src/
|
|
|
131
131
|
└── api.js
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
##
|
|
134
|
+
## database and API
|
|
135
135
|
|
|
136
136
|
```javascript
|
|
137
|
-
//
|
|
137
|
+
// database tables - snake_case
|
|
138
138
|
// user_profiles, order_items, payment_transactions
|
|
139
139
|
|
|
140
140
|
// API endpoints - kebab-case
|
|
@@ -162,7 +162,7 @@ const htmlContent = '<div>...</div>'
|
|
|
162
162
|
const xmlParser = new Parser()
|
|
163
163
|
|
|
164
164
|
// Bad
|
|
165
|
-
const userID = 123
|
|
165
|
+
const userID = 123 // ID should be Id
|
|
166
166
|
const APIURL = '/api' // Should be ApiUrl
|
|
167
167
|
const HTMLContent = '<div>...</div>'
|
|
168
168
|
```
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: riligar-dev-database
|
|
3
|
-
description:
|
|
3
|
+
description: database patterns for RiLiGar using Drizzle ORM + bun:sqlite. Use when setting up database connections, defining schemas, creating migrations, or writing queries. Covers SQLite on Fly.io volumes with the drizzle-kit workflow.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# database — Drizzle + bun:sqlite
|
|
7
7
|
|
|
8
8
|
> SQLite nativo no Bun. Zero drivers externos. Base de dados no volume do Fly.io (`/app/data`).
|
|
9
9
|
|
|
10
10
|
## Referências
|
|
11
11
|
|
|
12
|
-
| Arquivo
|
|
13
|
-
|
|
|
14
|
-
| [connection.md](references/connection.md) | Setup inicial: instalação, db.js, drizzle.config
|
|
15
|
-
| [schema.md](references/schema.md)
|
|
16
|
-
| [migrations.md](references/migrations.md) | Criar e executar migrations com drizzle-kit
|
|
17
|
-
| [queries.md](references/queries.md)
|
|
12
|
+
| Arquivo | Quando usar |
|
|
13
|
+
| ----------------------------------------- | ---------------------------------------------------- |
|
|
14
|
+
| [connection.md](references/connection.md) | Setup inicial: instalação, db.js, drizzle.config |
|
|
15
|
+
| [schema.md](references/schema.md) | Definir tabelas, tipos de colunas, relações |
|
|
16
|
+
| [migrations.md](references/migrations.md) | Criar e executar migrations com drizzle-kit |
|
|
17
|
+
| [queries.md](references/queries.md) | Select, insert, update, delete, queries com relações |
|
|
18
18
|
|
|
19
19
|
## Quick Start
|
|
20
20
|
|
|
21
21
|
```javascript
|
|
22
22
|
// database/db.js
|
|
23
23
|
import { drizzle } from 'drizzle-orm/bun-sqlite'
|
|
24
|
-
import
|
|
24
|
+
import database from 'bun:sqlite'
|
|
25
25
|
|
|
26
|
-
const sqlite = new
|
|
26
|
+
const sqlite = new database(process.env.DB_PATH ?? './database/database.db')
|
|
27
27
|
const db = drizzle({ client: sqlite })
|
|
28
28
|
|
|
29
29
|
export { db }
|
|
@@ -31,15 +31,15 @@ export { db }
|
|
|
31
31
|
|
|
32
32
|
## Regras
|
|
33
33
|
|
|
34
|
-
- **Caminho do banco:** `/app/data/database.db` em produção (volume Fly.io). `./
|
|
34
|
+
- **Caminho do banco:** `/app/data/database.db` em produção (volume Fly.io). `./database/database.db` em desenvolvimento.
|
|
35
35
|
- **Migrations sempre:** Use `drizzle-kit generate` + `drizzle-kit migrate`. Nunca edite migrations à mão.
|
|
36
36
|
- **Schema único:** Todas as tabelas em `database/schema.js`.
|
|
37
37
|
- **Migrations no startup:** Use `migrate()` no `index.js` antes de `.listen()`.
|
|
38
38
|
|
|
39
39
|
## Related Skills
|
|
40
40
|
|
|
41
|
-
| Need
|
|
42
|
-
|
|
|
43
|
-
| **Backend (Elysia)**
|
|
41
|
+
| Need | Skill |
|
|
42
|
+
| --------------------- | ------------------------------------- |
|
|
43
|
+
| **Backend (Elysia)** | @[.agent/skills/riligar-dev-manager] |
|
|
44
44
|
| **Payments (Stripe)** | @[.agent/skills/riligar-infra-stripe] |
|
|
45
|
-
| **Infrastructure**
|
|
45
|
+
| **Infrastructure** | @[.agent/skills/riligar-infra-fly] |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# database Connection
|
|
2
2
|
|
|
3
3
|
## Installation
|
|
4
4
|
|
|
@@ -14,15 +14,15 @@ No additional drivers needed — `bun:sqlite` is built into Bun.
|
|
|
14
14
|
```javascript
|
|
15
15
|
// database/db.js
|
|
16
16
|
import { drizzle } from 'drizzle-orm/bun-sqlite'
|
|
17
|
-
import
|
|
17
|
+
import database from 'bun:sqlite'
|
|
18
18
|
|
|
19
|
-
const sqlite = new
|
|
19
|
+
const sqlite = new database(process.env.DB_PATH ?? './database/database.db')
|
|
20
20
|
const db = drizzle({ client: sqlite })
|
|
21
21
|
|
|
22
22
|
export { db }
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
- Em **desenvolvimento**: `DB_PATH` não é definido → usa `./
|
|
25
|
+
- Em **desenvolvimento**: `DB_PATH` não é definido → usa `./database/database.db`
|
|
26
26
|
- Em **produção** (Fly.io): `fly secrets set DB_PATH=/app/data/database.db`
|
|
27
27
|
|
|
28
28
|
## drizzle.config.js
|
|
@@ -36,7 +36,7 @@ export default defineConfig({
|
|
|
36
36
|
schema: './database/schema.js',
|
|
37
37
|
out: './database/migrations',
|
|
38
38
|
dbCredentials: {
|
|
39
|
-
url: process.env.DB_PATH ?? './
|
|
39
|
+
url: process.env.DB_PATH ?? './database/database.db',
|
|
40
40
|
},
|
|
41
41
|
})
|
|
42
42
|
```
|
|
@@ -53,9 +53,7 @@ import { db } from './database/db'
|
|
|
53
53
|
await migrate(db, { migrationsFolder: './database/migrations' })
|
|
54
54
|
|
|
55
55
|
// ... resto do setup
|
|
56
|
-
const app = new Elysia()
|
|
57
|
-
.use(routes)
|
|
58
|
-
.listen(3000)
|
|
56
|
+
const app = new Elysia().use(routes).listen(3000)
|
|
59
57
|
```
|
|
60
58
|
|
|
61
59
|
Isso garante que o banco esteja sempre atualizado quando o servidor inicia no Fly.io.
|
|
@@ -17,10 +17,12 @@ import { eq, and, or, desc, asc } from 'drizzle-orm'
|
|
|
17
17
|
const allUsers = await db.select().from(users)
|
|
18
18
|
|
|
19
19
|
// Campos específicos
|
|
20
|
-
const names = await db
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const names = await db
|
|
21
|
+
.select({
|
|
22
|
+
id: users.id,
|
|
23
|
+
name: users.name,
|
|
24
|
+
})
|
|
25
|
+
.from(users)
|
|
24
26
|
```
|
|
25
27
|
|
|
26
28
|
### Filtrar
|
|
@@ -30,23 +32,24 @@ const names = await db.select({
|
|
|
30
32
|
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1)
|
|
31
33
|
|
|
32
34
|
// Múltiplos filtros (AND)
|
|
33
|
-
const results = await db
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
)
|
|
35
|
+
const results = await db
|
|
36
|
+
.select()
|
|
37
|
+
.from(users)
|
|
38
|
+
.where(and(eq(users.active, true), eq(users.plan, 'pro')))
|
|
39
39
|
|
|
40
40
|
// OR
|
|
41
|
-
const results = await db
|
|
42
|
-
|
|
43
|
-
)
|
|
41
|
+
const results = await db
|
|
42
|
+
.select()
|
|
43
|
+
.from(users)
|
|
44
|
+
.where(or(eq(users.id, '1'), eq(users.id, '2')))
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
### Ordenar e Paginar
|
|
47
48
|
|
|
48
49
|
```javascript
|
|
49
|
-
const page = await db
|
|
50
|
+
const page = await db
|
|
51
|
+
.select()
|
|
52
|
+
.from(posts)
|
|
50
53
|
.orderBy(desc(posts.createdAt))
|
|
51
54
|
.limit(10)
|
|
52
55
|
.offset(pageIndex * 10)
|
|
@@ -59,9 +62,7 @@ const page = await db.select().from(posts)
|
|
|
59
62
|
await db.insert(users).values({ name: 'Dan', email: 'dan@email.com' })
|
|
60
63
|
|
|
61
64
|
// Com retorno
|
|
62
|
-
const [user] = await db.insert(users)
|
|
63
|
-
.values({ name: 'Dan', email: 'dan@email.com' })
|
|
64
|
-
.returning()
|
|
65
|
+
const [user] = await db.insert(users).values({ name: 'Dan', email: 'dan@email.com' }).returning()
|
|
65
66
|
|
|
66
67
|
// Múltiplos
|
|
67
68
|
await db.insert(users).values([
|
|
@@ -70,7 +71,8 @@ await db.insert(users).values([
|
|
|
70
71
|
])
|
|
71
72
|
|
|
72
73
|
// Upsert (conflict handling)
|
|
73
|
-
await db
|
|
74
|
+
await db
|
|
75
|
+
.insert(users)
|
|
74
76
|
.values({ id: '1', name: 'Dan' })
|
|
75
77
|
.onConflictDoUpdate({
|
|
76
78
|
target: users.id,
|
|
@@ -81,15 +83,10 @@ await db.insert(users)
|
|
|
81
83
|
## Update
|
|
82
84
|
|
|
83
85
|
```javascript
|
|
84
|
-
await db.update(users)
|
|
85
|
-
.set({ name: 'Mr. Dan', updatedAt: new Date() })
|
|
86
|
-
.where(eq(users.id, userId))
|
|
86
|
+
await db.update(users).set({ name: 'Mr. Dan', updatedAt: new Date() }).where(eq(users.id, userId))
|
|
87
87
|
|
|
88
88
|
// Com retorno
|
|
89
|
-
const [updated] = await db.update(users)
|
|
90
|
-
.set({ plan: 'pro' })
|
|
91
|
-
.where(eq(users.id, userId))
|
|
92
|
-
.returning()
|
|
89
|
+
const [updated] = await db.update(users).set({ plan: 'pro' }).where(eq(users.id, userId)).returning()
|
|
93
90
|
```
|
|
94
91
|
|
|
95
92
|
## Delete
|
|
@@ -98,9 +95,7 @@ const [updated] = await db.update(users)
|
|
|
98
95
|
await db.delete(users).where(eq(users.id, userId))
|
|
99
96
|
|
|
100
97
|
// Com retorno
|
|
101
|
-
const [deleted] = await db.delete(users)
|
|
102
|
-
.where(eq(users.id, userId))
|
|
103
|
-
.returning()
|
|
98
|
+
const [deleted] = await db.delete(users).where(eq(users.id, userId)).returning()
|
|
104
99
|
```
|
|
105
100
|
|
|
106
101
|
## Queries com Relações
|
|
@@ -159,7 +154,8 @@ export async function createUser(data) {
|
|
|
159
154
|
}
|
|
160
155
|
|
|
161
156
|
export async function updateUser(id, data) {
|
|
162
|
-
const [user] = await db
|
|
157
|
+
const [user] = await db
|
|
158
|
+
.update(users)
|
|
163
159
|
.set({ ...data, updatedAt: new Date() })
|
|
164
160
|
.where(eq(users.id, id))
|
|
165
161
|
.returning()
|
|
@@ -8,37 +8,41 @@ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
|
|
|
8
8
|
import { relations } from 'drizzle-orm'
|
|
9
9
|
|
|
10
10
|
export const users = sqliteTable('users', {
|
|
11
|
-
id: text('id')
|
|
11
|
+
id: text('id')
|
|
12
|
+
.primaryKey()
|
|
13
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
12
14
|
email: text('email').notNull().unique(),
|
|
13
15
|
name: text('name'),
|
|
14
|
-
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
16
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
17
|
+
.notNull()
|
|
18
|
+
.$defaultFn(() => new Date()),
|
|
15
19
|
updatedAt: integer('updated_at', { mode: 'timestamp' }),
|
|
16
20
|
})
|
|
17
21
|
```
|
|
18
22
|
|
|
19
23
|
## Tipos de Colunas
|
|
20
24
|
|
|
21
|
-
| Tipo
|
|
22
|
-
|
|
|
23
|
-
| `text()`
|
|
24
|
-
| `integer()` | Números, booleans, timestamps | `integer('age')`
|
|
25
|
-
| `real()`
|
|
26
|
-
| `blob()`
|
|
27
|
-
| `numeric()` | Valores numéricos precisos
|
|
25
|
+
| Tipo | Uso | Exemplo |
|
|
26
|
+
| ----------- | ----------------------------- | ------------------------ |
|
|
27
|
+
| `text()` | Strings, UUIDs, JSON | `text('name').notNull()` |
|
|
28
|
+
| `integer()` | Números, booleans, timestamps | `integer('age')` |
|
|
29
|
+
| `real()` | Decimais (float) | `real('price')` |
|
|
30
|
+
| `blob()` | Dados binários | `blob('avatar')` |
|
|
31
|
+
| `numeric()` | Valores numéricos precisos | `numeric('amount')` |
|
|
28
32
|
|
|
29
33
|
### Modes do `integer()`
|
|
30
34
|
|
|
31
35
|
```javascript
|
|
32
|
-
integer('count')
|
|
33
|
-
integer('active', { mode: 'boolean' })
|
|
34
|
-
integer('created_at', { mode: 'timestamp' })
|
|
36
|
+
integer('count') // número
|
|
37
|
+
integer('active', { mode: 'boolean' }) // boolean (0/1)
|
|
38
|
+
integer('created_at', { mode: 'timestamp' }) // Date (segundos)
|
|
35
39
|
integer('created_at', { mode: 'timestamp_ms' }) // Date (milissegundos)
|
|
36
40
|
```
|
|
37
41
|
|
|
38
42
|
### JSON via `text()`
|
|
39
43
|
|
|
40
44
|
```javascript
|
|
41
|
-
text('metadata', { mode: 'json' })
|
|
45
|
+
text('metadata', { mode: 'json' }) // armazena JSON como text
|
|
42
46
|
```
|
|
43
47
|
|
|
44
48
|
## Primary Key
|
|
@@ -55,11 +59,17 @@ id: integer('id').primaryKey({ autoIncrement: true }),
|
|
|
55
59
|
|
|
56
60
|
```javascript
|
|
57
61
|
export const posts = sqliteTable('posts', {
|
|
58
|
-
id: text('id')
|
|
59
|
-
|
|
62
|
+
id: text('id')
|
|
63
|
+
.primaryKey()
|
|
64
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
65
|
+
authorId: text('author_id')
|
|
66
|
+
.notNull()
|
|
67
|
+
.references(() => users.id),
|
|
60
68
|
title: text('title').notNull(),
|
|
61
69
|
body: text('body'),
|
|
62
|
-
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
70
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
71
|
+
.notNull()
|
|
72
|
+
.$defaultFn(() => new Date()),
|
|
63
73
|
})
|
|
64
74
|
```
|
|
65
75
|
|
|
@@ -84,8 +94,12 @@ export const postsRelations = relations(posts, ({ one }) => ({
|
|
|
84
94
|
|
|
85
95
|
```javascript
|
|
86
96
|
export const usersToGroups = sqliteTable('users_to_groups', {
|
|
87
|
-
userId: text('user_id')
|
|
88
|
-
|
|
97
|
+
userId: text('user_id')
|
|
98
|
+
.notNull()
|
|
99
|
+
.references(() => users.id),
|
|
100
|
+
groupId: text('group_id')
|
|
101
|
+
.notNull()
|
|
102
|
+
.references(() => groups.id),
|
|
89
103
|
})
|
|
90
104
|
|
|
91
105
|
export const usersRelations = relations(users, ({ many }) => ({
|
|
@@ -22,21 +22,21 @@ const app = new Elysia()
|
|
|
22
22
|
.post('/users', ({ body }) => createUser(body), {
|
|
23
23
|
body: t.Object({
|
|
24
24
|
name: t.String(),
|
|
25
|
-
email: t.String({ format: 'email' })
|
|
26
|
-
})
|
|
25
|
+
email: t.String({ format: 'email' }),
|
|
26
|
+
}),
|
|
27
27
|
})
|
|
28
28
|
.listen(3000)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## Content Map
|
|
32
32
|
|
|
33
|
-
| File
|
|
34
|
-
|
|
|
35
|
-
| [elysia-basics.md](references/elysia-basics.md)
|
|
36
|
-
| [elysia-plugins.md](references/elysia-plugins.md)
|
|
37
|
-
| [elysia-validation.md](references/elysia-validation.md) | TypeBox validation (body, query, params) | Input validation
|
|
38
|
-
| [elysia-lifecycle.md](references/elysia-lifecycle.md)
|
|
39
|
-
| [elysia-patterns.md](references/elysia-patterns.md)
|
|
33
|
+
| File | Description | When to Read |
|
|
34
|
+
| ------------------------------------------------------- | ---------------------------------------- | ----------------------- |
|
|
35
|
+
| [elysia-basics.md](references/elysia-basics.md) | Setup, routes, handlers, context | Starting new project |
|
|
36
|
+
| [elysia-plugins.md](references/elysia-plugins.md) | Plugins, guards, modular design | Organizing code |
|
|
37
|
+
| [elysia-validation.md](references/elysia-validation.md) | TypeBox validation (body, query, params) | Input validation |
|
|
38
|
+
| [elysia-lifecycle.md](references/elysia-lifecycle.md) | Hooks (onBeforeHandle, onError, etc.) | Middleware, auth checks |
|
|
39
|
+
| [elysia-patterns.md](references/elysia-patterns.md) | REST patterns, responses, pagination | API design |
|
|
40
40
|
|
|
41
41
|
## Project Structure
|
|
42
42
|
|
|
@@ -61,13 +61,13 @@ src/
|
|
|
61
61
|
|
|
62
62
|
## Dependencies
|
|
63
63
|
|
|
64
|
-
| Pacote
|
|
65
|
-
|
|
66
|
-
| `bun`
|
|
67
|
-
| `elysia`
|
|
68
|
-
| `bun:sqlite`
|
|
69
|
-
| `drizzle-orm` | latest
|
|
70
|
-
| `bun:s3`
|
|
64
|
+
| Pacote | Versão | Descrição |
|
|
65
|
+
| ------------- | ------- | -------------- |
|
|
66
|
+
| `bun` | latest | Runtime |
|
|
67
|
+
| `elysia` | latest | Framework HTTP |
|
|
68
|
+
| `bun:sqlite` | builtin | SQLite driver |
|
|
69
|
+
| `drizzle-orm` | latest | ORM |
|
|
70
|
+
| `bun:s3` | latest | S3/R2 Storage |
|
|
71
71
|
|
|
72
72
|
## Core Patterns
|
|
73
73
|
|
|
@@ -84,8 +84,8 @@ export const userRoutes = new Elysia({ prefix: '/users' })
|
|
|
84
84
|
.post('/', ({ body }) => createUser(body), {
|
|
85
85
|
body: t.Object({
|
|
86
86
|
name: t.String({ minLength: 1 }),
|
|
87
|
-
email: t.String({ format: 'email' })
|
|
88
|
-
})
|
|
87
|
+
email: t.String({ format: 'email' }),
|
|
88
|
+
}),
|
|
89
89
|
})
|
|
90
90
|
```
|
|
91
91
|
|
|
@@ -112,11 +112,11 @@ console.log(`Server running at ${app.server?.url}`)
|
|
|
112
112
|
|
|
113
113
|
## Related Skills
|
|
114
114
|
|
|
115
|
-
| Need
|
|
116
|
-
|
|
|
115
|
+
| Need | Skill |
|
|
116
|
+
| ------------------ | ---------------------------------------- |
|
|
117
117
|
| **Authentication** | @[.agent/skills/riligar-dev-auth-elysia] |
|
|
118
|
-
| **
|
|
119
|
-
| **Infrastructure** | @[.agent/skills/riligar-infra-fly]
|
|
118
|
+
| **database** | @[.agent/skills/riligar-dev-database] |
|
|
119
|
+
| **Infrastructure** | @[.agent/skills/riligar-infra-fly] |
|
|
120
120
|
|
|
121
121
|
## Decision Checklist
|
|
122
122
|
|
|
@@ -126,14 +126,14 @@ Before building an API:
|
|
|
126
126
|
- [ ] Planned validation for all inputs?
|
|
127
127
|
- [ ] Error handling configured?
|
|
128
128
|
- [ ] Auth middleware needed? → Use `riligar-dev-auth-elysia`
|
|
129
|
-
- [ ]
|
|
129
|
+
- [ ] database connection setup? → Use `riligar-dev-database`
|
|
130
130
|
|
|
131
131
|
## Anti-Patterns
|
|
132
132
|
|
|
133
|
-
| Don't
|
|
134
|
-
|
|
|
135
|
-
| Put business logic in handlers
|
|
136
|
-
| Skip input validation
|
|
137
|
-
| Ignore error handling
|
|
138
|
-
| Create monolithic files
|
|
133
|
+
| Don't | Do |
|
|
134
|
+
| -------------------------------- | ------------------------ |
|
|
135
|
+
| Put business logic in handlers | Extract to `services/` |
|
|
136
|
+
| Skip input validation | Use TypeBox (`t.Object`) |
|
|
137
|
+
| Ignore error handling | Use `onError` lifecycle |
|
|
138
|
+
| Create monolithic files | Split into plugins |
|
|
139
139
|
| Use verbs in routes (`/getUser`) | Use nouns (`/users/:id`) |
|
|
@@ -21,8 +21,9 @@ Pergunte ao usuário:
|
|
|
21
21
|
> Você pode encontrá-las em: https://dashboard.stripe.com/apikeys
|
|
22
22
|
>
|
|
23
23
|
> Por favor, me forneça:
|
|
24
|
-
>
|
|
25
|
-
>
|
|
24
|
+
>
|
|
25
|
+
> 1. **Publishable Key** (pk*live*... ou pk*test*...)
|
|
26
|
+
> 2. **Secret Key** (sk*live*... ou sk*test*...)
|
|
26
27
|
|
|
27
28
|
Aguarde as chaves antes de prosseguir.
|
|
28
29
|
|
|
@@ -36,6 +37,7 @@ Após receber as chaves, pergunte:
|
|
|
36
37
|
> 2. **Quais planos/produtos** você quer oferecer?
|
|
37
38
|
>
|
|
38
39
|
> Exemplo de resposta:
|
|
40
|
+
>
|
|
39
41
|
> - Assinatura mensal
|
|
40
42
|
> - Plano Starter: R$ 29/mês (5 projetos, suporte email)
|
|
41
43
|
> - Plano Pro: R$ 99/mês (ilimitado, suporte prioritário)
|
|
@@ -57,17 +59,17 @@ async function setupProducts() {
|
|
|
57
59
|
{
|
|
58
60
|
name: 'Plano Starter',
|
|
59
61
|
description: '5 projetos, suporte email',
|
|
60
|
-
price: 2900,
|
|
62
|
+
price: 2900, // R$ 29,00 em centavos
|
|
61
63
|
interval: 'month',
|
|
62
|
-
features: ['5 projetos', 'Suporte email', '1GB storage']
|
|
64
|
+
features: ['5 projetos', 'Suporte email', '1GB storage'],
|
|
63
65
|
},
|
|
64
66
|
{
|
|
65
67
|
name: 'Plano Pro',
|
|
66
68
|
description: 'Ilimitado, suporte prioritário',
|
|
67
|
-
price: 9900,
|
|
69
|
+
price: 9900, // R$ 99,00 em centavos
|
|
68
70
|
interval: 'month',
|
|
69
|
-
features: ['Projetos ilimitados', 'Suporte prioritário', '10GB storage']
|
|
70
|
-
}
|
|
71
|
+
features: ['Projetos ilimitados', 'Suporte prioritário', '10GB storage'],
|
|
72
|
+
},
|
|
71
73
|
]
|
|
72
74
|
|
|
73
75
|
console.log('Criando produtos no Stripe...\n')
|
|
@@ -76,14 +78,14 @@ async function setupProducts() {
|
|
|
76
78
|
const stripeProduct = await stripe.products.create({
|
|
77
79
|
name: product.name,
|
|
78
80
|
description: product.description,
|
|
79
|
-
metadata: { features: JSON.stringify(product.features) }
|
|
81
|
+
metadata: { features: JSON.stringify(product.features) },
|
|
80
82
|
})
|
|
81
83
|
|
|
82
84
|
const stripePrice = await stripe.prices.create({
|
|
83
85
|
product: stripeProduct.id,
|
|
84
86
|
unit_amount: product.price,
|
|
85
87
|
currency: 'brl',
|
|
86
|
-
recurring: product.interval ? { interval: product.interval } : undefined
|
|
88
|
+
recurring: product.interval ? { interval: product.interval } : undefined,
|
|
87
89
|
})
|
|
88
90
|
|
|
89
91
|
console.log(`✓ ${product.name}`)
|
|
@@ -111,6 +113,7 @@ Após executar o script, peça:
|
|
|
111
113
|
Com as chaves e Price IDs, configure os arquivos de ambiente:
|
|
112
114
|
|
|
113
115
|
**Backend: `.env.development` e `.env.production`**
|
|
116
|
+
|
|
114
117
|
```bash
|
|
115
118
|
# .env.development (chaves de teste)
|
|
116
119
|
STRIPE_SECRET_KEY=sk_test_...
|
|
@@ -122,6 +125,7 @@ STRIPE_WEBHOOK_SECRET=whsec_...
|
|
|
122
125
|
```
|
|
123
126
|
|
|
124
127
|
**Frontend: `.env.development` e `.env.production`**
|
|
128
|
+
|
|
125
129
|
```bash
|
|
126
130
|
# .env.development
|
|
127
131
|
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_...
|
|
@@ -130,7 +134,7 @@ VITE_STRIPE_PUBLISHABLE_KEY=pk_test_...
|
|
|
130
134
|
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_...
|
|
131
135
|
```
|
|
132
136
|
|
|
133
|
-
### Step 6: Configurar
|
|
137
|
+
### Step 6: Configurar database
|
|
134
138
|
|
|
135
139
|
Gere a migration para adicionar campos do Stripe:
|
|
136
140
|
|
|
@@ -153,29 +157,29 @@ Instrua o usuário:
|
|
|
153
157
|
> 2. Clique em "Add endpoint"
|
|
154
158
|
> 3. URL: `https://seu-dominio.com/api/webhook`
|
|
155
159
|
> 4. Selecione os eventos:
|
|
156
|
-
>
|
|
157
|
-
>
|
|
158
|
-
>
|
|
159
|
-
>
|
|
160
|
-
>
|
|
161
|
-
> 5. Copie o "Signing secret" (
|
|
160
|
+
> - `checkout.session.completed`
|
|
161
|
+
> - `customer.subscription.updated`
|
|
162
|
+
> - `customer.subscription.deleted`
|
|
163
|
+
> - `invoice.paid`
|
|
164
|
+
> - `invoice.payment_failed`
|
|
165
|
+
> 5. Copie o "Signing secret" (whsec\_...)
|
|
162
166
|
> 6. Adicione ao `.env.development` e `.env.production`
|
|
163
167
|
|
|
164
168
|
### Step 8: Gerar Código
|
|
165
169
|
|
|
166
170
|
Gere todos os arquivos necessários usando os templates de [assets/](assets/):
|
|
167
171
|
|
|
168
|
-
| Arquivo
|
|
169
|
-
|
|
|
170
|
-
| `plugins/stripe.js`
|
|
171
|
-
| `routes/billing.js`
|
|
172
|
-
| `routes/webhook.js`
|
|
173
|
-
| `services/billing.js`
|
|
174
|
-
| `config/stripe-prices.js`
|
|
175
|
-
| `config/plans.js`
|
|
176
|
-
| `pages/Pricing.jsx`
|
|
177
|
-
| `components/BillingSettings.jsx` | stripe-client.js (seção 4)
|
|
178
|
-
| `hooks/useSubscription.js`
|
|
172
|
+
| Arquivo | Baseado em |
|
|
173
|
+
| -------------------------------- | ------------------------------- |
|
|
174
|
+
| `plugins/stripe.js` | stripe-server.js (seção 1) |
|
|
175
|
+
| `routes/billing.js` | stripe-server.js (seção 2) |
|
|
176
|
+
| `routes/webhook.js` | stripe-server.js (seção 3) |
|
|
177
|
+
| `services/billing.js` | stripe-server.js (seção 4) |
|
|
178
|
+
| `config/stripe-prices.js` | Price IDs coletados (Step 9) |
|
|
179
|
+
| `config/plans.js` | PLAN_MAP + PLAN_LIMITS (Step 9) |
|
|
180
|
+
| `pages/Pricing.jsx` | stripe-client.js (seção 3) |
|
|
181
|
+
| `components/BillingSettings.jsx` | stripe-client.js (seção 4) |
|
|
182
|
+
| `hooks/useSubscription.js` | stripe-client.js (seção 2) |
|
|
179
183
|
|
|
180
184
|
### Step 9: Criar Configs de Planos e Preços
|
|
181
185
|
|
|
@@ -188,24 +192,24 @@ export const STRIPE_PRICES = {
|
|
|
188
192
|
priceId: 'price_COLETADO_STARTER',
|
|
189
193
|
name: 'Starter',
|
|
190
194
|
price: 29,
|
|
191
|
-
features: ['5 projetos', 'Suporte email', '1GB storage']
|
|
195
|
+
features: ['5 projetos', 'Suporte email', '1GB storage'],
|
|
192
196
|
},
|
|
193
197
|
pro: {
|
|
194
198
|
priceId: 'price_COLETADO_PRO',
|
|
195
199
|
name: 'Pro',
|
|
196
200
|
price: 99,
|
|
197
|
-
features: ['Projetos ilimitados', 'Suporte prioritário', '10GB storage']
|
|
201
|
+
features: ['Projetos ilimitados', 'Suporte prioritário', '10GB storage'],
|
|
198
202
|
},
|
|
199
203
|
enterprise: {
|
|
200
204
|
priceId: 'price_COLETADO_ENTERPRISE',
|
|
201
205
|
name: 'Enterprise',
|
|
202
206
|
price: 299,
|
|
203
|
-
features: ['Tudo do Pro', 'Storage ilimitado', 'SLA garantido']
|
|
204
|
-
}
|
|
207
|
+
features: ['Tudo do Pro', 'Storage ilimitado', 'SLA garantido'],
|
|
208
|
+
},
|
|
205
209
|
}
|
|
206
210
|
|
|
207
|
-
export const getPrice =
|
|
208
|
-
export const getPriceId =
|
|
211
|
+
export const getPrice = plan => STRIPE_PRICES[plan]
|
|
212
|
+
export const getPriceId = plan => STRIPE_PRICES[plan]?.priceId
|
|
209
213
|
```
|
|
210
214
|
|
|
211
215
|
**B) Arquivo de mapeamento e limites (config/plans.js):**
|
|
@@ -215,9 +219,9 @@ export const getPriceId = (plan) => STRIPE_PRICES[plan]?.priceId
|
|
|
215
219
|
|
|
216
220
|
// Mapeia Price IDs do Stripe para nomes de planos internos
|
|
217
221
|
export const PLAN_MAP = {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
222
|
+
price_COLETADO_STARTER: 'starter',
|
|
223
|
+
price_COLETADO_PRO: 'pro',
|
|
224
|
+
price_COLETADO_ENTERPRISE: 'enterprise',
|
|
221
225
|
}
|
|
222
226
|
|
|
223
227
|
// Define limites de features por plano
|
|
@@ -283,12 +287,12 @@ stripe trigger checkout.session.completed
|
|
|
283
287
|
|
|
284
288
|
## Specialized Guides
|
|
285
289
|
|
|
286
|
-
| Guide
|
|
287
|
-
|
|
|
288
|
-
| [stripe-elysia.md](references/stripe-elysia.md)
|
|
289
|
-
| [stripe-react.md](references/stripe-react.md)
|
|
290
|
-
| [stripe-webhooks.md](references/stripe-webhooks.md) | Handlers de eventos
|
|
291
|
-
| [stripe-database.md](references/stripe-database.md) | Schema Drizzle
|
|
290
|
+
| Guide | Content |
|
|
291
|
+
| --------------------------------------------------- | ------------------------- |
|
|
292
|
+
| [stripe-elysia.md](references/stripe-elysia.md) | Backend routes completas |
|
|
293
|
+
| [stripe-react.md](references/stripe-react.md) | Componentes React/Mantine |
|
|
294
|
+
| [stripe-webhooks.md](references/stripe-webhooks.md) | Handlers de eventos |
|
|
295
|
+
| [stripe-database.md](references/stripe-database.md) | Schema Drizzle |
|
|
292
296
|
|
|
293
297
|
---
|
|
294
298
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Stripe
|
|
1
|
+
# Stripe database Patterns (Drizzle)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
database schemas and queries for Stripe billing with SQLite and Drizzle ORM.
|
|
4
4
|
|
|
5
5
|
## Schema
|
|
6
6
|
|
|
@@ -18,13 +18,15 @@ export const users = sqliteTable('users', {
|
|
|
18
18
|
// Stripe fields
|
|
19
19
|
stripeCustomerId: text('stripe_customer_id').unique(),
|
|
20
20
|
stripeSubscriptionId: text('stripe_subscription_id').unique(),
|
|
21
|
-
plan: text('plan').default('free'),
|
|
22
|
-
subscriptionStatus: text('subscription_status'),
|
|
21
|
+
plan: text('plan').default('free'), // free, starter, pro, enterprise
|
|
22
|
+
subscriptionStatus: text('subscription_status'), // active, past_due, canceled, trialing
|
|
23
23
|
currentPeriodEnd: integer('current_period_end', { mode: 'timestamp' }),
|
|
24
24
|
cancelAtPeriodEnd: integer('cancel_at_period_end', { mode: 'boolean' }).default(false),
|
|
25
25
|
|
|
26
|
-
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
27
|
-
|
|
26
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
27
|
+
.notNull()
|
|
28
|
+
.$defaultFn(() => new Date()),
|
|
29
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }),
|
|
28
30
|
})
|
|
29
31
|
```
|
|
30
32
|
|
|
@@ -32,13 +34,19 @@ export const users = sqliteTable('users', {
|
|
|
32
34
|
|
|
33
35
|
```javascript
|
|
34
36
|
export const invoices = sqliteTable('invoices', {
|
|
35
|
-
id: text('id')
|
|
36
|
-
|
|
37
|
+
id: text('id')
|
|
38
|
+
.primaryKey()
|
|
39
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
40
|
+
userId: text('user_id')
|
|
41
|
+
.references(() => users.id)
|
|
42
|
+
.notNull(),
|
|
37
43
|
stripeInvoiceId: text('stripe_invoice_id').unique().notNull(),
|
|
38
|
-
amount: integer('amount').notNull(),
|
|
39
|
-
status: text('status').notNull(),
|
|
40
|
-
url: text('url'),
|
|
41
|
-
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
44
|
+
amount: integer('amount').notNull(), // in cents
|
|
45
|
+
status: text('status').notNull(), // paid, open, void
|
|
46
|
+
url: text('url'), // hosted invoice URL
|
|
47
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
48
|
+
.notNull()
|
|
49
|
+
.$defaultFn(() => new Date()),
|
|
42
50
|
})
|
|
43
51
|
```
|
|
44
52
|
|
|
@@ -46,10 +54,12 @@ export const invoices = sqliteTable('invoices', {
|
|
|
46
54
|
|
|
47
55
|
```javascript
|
|
48
56
|
export const webhookEvents = sqliteTable('webhook_events', {
|
|
49
|
-
id: text('id')
|
|
57
|
+
id: text('id')
|
|
58
|
+
.primaryKey()
|
|
59
|
+
.$defaultFn(() => crypto.randomUUID()),
|
|
50
60
|
stripeEventId: text('stripe_event_id').unique().notNull(),
|
|
51
61
|
type: text('type').notNull(),
|
|
52
|
-
processedAt: integer('processed_at', { mode: 'timestamp' }).notNull()
|
|
62
|
+
processedAt: integer('processed_at', { mode: 'timestamp' }).notNull(),
|
|
53
63
|
})
|
|
54
64
|
```
|
|
55
65
|
|
|
@@ -61,10 +71,10 @@ export const products = sqliteTable('products', {
|
|
|
61
71
|
stripePriceId: text('stripe_price_id').unique().notNull(),
|
|
62
72
|
name: text('name').notNull(),
|
|
63
73
|
description: text('description'),
|
|
64
|
-
amount: integer('amount').notNull(),
|
|
65
|
-
interval: text('interval'),
|
|
74
|
+
amount: integer('amount').notNull(), // in cents
|
|
75
|
+
interval: text('interval'), // month, year, null for one-time
|
|
66
76
|
active: integer('active', { mode: 'boolean' }).default(true),
|
|
67
|
-
features: text('features', { mode: 'json' })
|
|
77
|
+
features: text('features', { mode: 'json' }), // ["feature1", "feature2"]
|
|
68
78
|
})
|
|
69
79
|
```
|
|
70
80
|
|
|
@@ -78,10 +88,7 @@ import { users } from '../database/schema'
|
|
|
78
88
|
import { eq } from 'drizzle-orm'
|
|
79
89
|
|
|
80
90
|
export async function getUserWithSubscription(userId) {
|
|
81
|
-
const [user] = await db.select()
|
|
82
|
-
.from(users)
|
|
83
|
-
.where(eq(users.id, userId))
|
|
84
|
-
.limit(1)
|
|
91
|
+
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1)
|
|
85
92
|
|
|
86
93
|
return user
|
|
87
94
|
}
|
|
@@ -91,10 +98,11 @@ export async function getUserWithSubscription(userId) {
|
|
|
91
98
|
|
|
92
99
|
```javascript
|
|
93
100
|
export async function hasActiveSubscription(userId) {
|
|
94
|
-
const [user] = await db
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
const [user] = await db
|
|
102
|
+
.select({
|
|
103
|
+
status: users.subscriptionStatus,
|
|
104
|
+
periodEnd: users.currentPeriodEnd,
|
|
105
|
+
})
|
|
98
106
|
.from(users)
|
|
99
107
|
.where(eq(users.id, userId))
|
|
100
108
|
.limit(1)
|
|
@@ -110,10 +118,7 @@ export async function hasActiveSubscription(userId) {
|
|
|
110
118
|
|
|
111
119
|
```javascript
|
|
112
120
|
export async function getUserByStripeCustomer(stripeCustomerId) {
|
|
113
|
-
const [user] = await db.select()
|
|
114
|
-
.from(users)
|
|
115
|
-
.where(eq(users.stripeCustomerId, stripeCustomerId))
|
|
116
|
-
.limit(1)
|
|
121
|
+
const [user] = await db.select().from(users).where(eq(users.stripeCustomerId, stripeCustomerId)).limit(1)
|
|
117
122
|
|
|
118
123
|
return user
|
|
119
124
|
}
|
|
@@ -123,10 +128,7 @@ export async function getUserByStripeCustomer(stripeCustomerId) {
|
|
|
123
128
|
|
|
124
129
|
```javascript
|
|
125
130
|
export async function getUserBySubscription(stripeSubscriptionId) {
|
|
126
|
-
const [user] = await db.select()
|
|
127
|
-
.from(users)
|
|
128
|
-
.where(eq(users.stripeSubscriptionId, stripeSubscriptionId))
|
|
129
|
-
.limit(1)
|
|
131
|
+
const [user] = await db.select().from(users).where(eq(users.stripeSubscriptionId, stripeSubscriptionId)).limit(1)
|
|
130
132
|
|
|
131
133
|
return user
|
|
132
134
|
}
|
|
@@ -136,7 +138,8 @@ export async function getUserBySubscription(stripeSubscriptionId) {
|
|
|
136
138
|
|
|
137
139
|
```javascript
|
|
138
140
|
export async function updateUserSubscription(userId, data) {
|
|
139
|
-
await db
|
|
141
|
+
await db
|
|
142
|
+
.update(users)
|
|
140
143
|
.set({
|
|
141
144
|
stripeCustomerId: data.customerId,
|
|
142
145
|
stripeSubscriptionId: data.subscriptionId,
|
|
@@ -144,7 +147,7 @@ export async function updateUserSubscription(userId, data) {
|
|
|
144
147
|
subscriptionStatus: data.status,
|
|
145
148
|
currentPeriodEnd: data.periodEnd,
|
|
146
149
|
cancelAtPeriodEnd: data.cancelAtPeriodEnd,
|
|
147
|
-
updatedAt: new Date()
|
|
150
|
+
updatedAt: new Date(),
|
|
148
151
|
})
|
|
149
152
|
.where(eq(users.id, userId))
|
|
150
153
|
}
|
|
@@ -154,12 +157,13 @@ export async function updateUserSubscription(userId, data) {
|
|
|
154
157
|
|
|
155
158
|
```javascript
|
|
156
159
|
export async function cancelSubscriptionInDb(subscriptionId) {
|
|
157
|
-
await db
|
|
160
|
+
await db
|
|
161
|
+
.update(users)
|
|
158
162
|
.set({
|
|
159
163
|
plan: 'free',
|
|
160
164
|
subscriptionStatus: 'canceled',
|
|
161
165
|
stripeSubscriptionId: null,
|
|
162
|
-
updatedAt: new Date()
|
|
166
|
+
updatedAt: new Date(),
|
|
163
167
|
})
|
|
164
168
|
.where(eq(users.stripeSubscriptionId, subscriptionId))
|
|
165
169
|
}
|
|
@@ -171,11 +175,7 @@ export async function cancelSubscriptionInDb(subscriptionId) {
|
|
|
171
175
|
import { desc } from 'drizzle-orm'
|
|
172
176
|
|
|
173
177
|
export async function getUserInvoices(userId, limit = 10) {
|
|
174
|
-
return db.select()
|
|
175
|
-
.from(invoices)
|
|
176
|
-
.where(eq(invoices.userId, userId))
|
|
177
|
-
.orderBy(desc(invoices.createdAt))
|
|
178
|
-
.limit(limit)
|
|
178
|
+
return db.select().from(invoices).where(eq(invoices.userId, userId)).orderBy(desc(invoices.createdAt)).limit(limit)
|
|
179
179
|
}
|
|
180
180
|
```
|
|
181
181
|
|
|
@@ -183,10 +183,7 @@ export async function getUserInvoices(userId, limit = 10) {
|
|
|
183
183
|
|
|
184
184
|
```javascript
|
|
185
185
|
export async function isWebhookProcessed(eventId) {
|
|
186
|
-
const [event] = await db.select()
|
|
187
|
-
.from(webhookEvents)
|
|
188
|
-
.where(eq(webhookEvents.stripeEventId, eventId))
|
|
189
|
-
.limit(1)
|
|
186
|
+
const [event] = await db.select().from(webhookEvents).where(eq(webhookEvents.stripeEventId, eventId)).limit(1)
|
|
190
187
|
|
|
191
188
|
return !!event
|
|
192
189
|
}
|
|
@@ -195,7 +192,7 @@ export async function markWebhookProcessed(eventId, type) {
|
|
|
195
192
|
await db.insert(webhookEvents).values({
|
|
196
193
|
stripeEventId: eventId,
|
|
197
194
|
type,
|
|
198
|
-
processedAt: new Date()
|
|
195
|
+
processedAt: new Date(),
|
|
199
196
|
})
|
|
200
197
|
}
|
|
201
198
|
```
|
|
@@ -274,42 +271,39 @@ export const billingService = {
|
|
|
274
271
|
|
|
275
272
|
// Get user
|
|
276
273
|
async getUser(userId) {
|
|
277
|
-
const [user] = await db.select()
|
|
278
|
-
.from(users)
|
|
279
|
-
.where(eq(users.id, userId))
|
|
280
|
-
.limit(1)
|
|
274
|
+
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1)
|
|
281
275
|
return user
|
|
282
276
|
},
|
|
283
277
|
|
|
284
278
|
// Link Stripe customer
|
|
285
279
|
async linkStripeCustomer(userId, stripeCustomerId) {
|
|
286
|
-
await db.update(users)
|
|
287
|
-
.set({ stripeCustomerId, updatedAt: new Date() })
|
|
288
|
-
.where(eq(users.id, userId))
|
|
280
|
+
await db.update(users).set({ stripeCustomerId, updatedAt: new Date() }).where(eq(users.id, userId))
|
|
289
281
|
},
|
|
290
282
|
|
|
291
283
|
// Activate subscription
|
|
292
284
|
async activateSubscription(userId, { subscriptionId, plan, periodEnd }) {
|
|
293
|
-
await db
|
|
285
|
+
await db
|
|
286
|
+
.update(users)
|
|
294
287
|
.set({
|
|
295
288
|
stripeSubscriptionId: subscriptionId,
|
|
296
289
|
plan,
|
|
297
290
|
subscriptionStatus: 'active',
|
|
298
291
|
currentPeriodEnd: periodEnd,
|
|
299
292
|
cancelAtPeriodEnd: false,
|
|
300
|
-
updatedAt: new Date()
|
|
293
|
+
updatedAt: new Date(),
|
|
301
294
|
})
|
|
302
295
|
.where(eq(users.id, userId))
|
|
303
296
|
},
|
|
304
297
|
|
|
305
298
|
// Deactivate subscription
|
|
306
299
|
async deactivateSubscription(subscriptionId) {
|
|
307
|
-
await db
|
|
300
|
+
await db
|
|
301
|
+
.update(users)
|
|
308
302
|
.set({
|
|
309
303
|
plan: 'free',
|
|
310
304
|
subscriptionStatus: 'canceled',
|
|
311
305
|
stripeSubscriptionId: null,
|
|
312
|
-
updatedAt: new Date()
|
|
306
|
+
updatedAt: new Date(),
|
|
313
307
|
})
|
|
314
308
|
.where(eq(users.stripeSubscriptionId, subscriptionId))
|
|
315
309
|
},
|
|
@@ -321,9 +315,9 @@ export const billingService = {
|
|
|
321
315
|
stripeInvoiceId: stripeInvoice.id,
|
|
322
316
|
amount: stripeInvoice.amount_paid,
|
|
323
317
|
status: stripeInvoice.status,
|
|
324
|
-
url: stripeInvoice.hosted_invoice_url
|
|
318
|
+
url: stripeInvoice.hosted_invoice_url,
|
|
325
319
|
})
|
|
326
|
-
}
|
|
320
|
+
},
|
|
327
321
|
}
|
|
328
322
|
```
|
|
329
323
|
|
|
@@ -334,28 +328,28 @@ export const billingService = {
|
|
|
334
328
|
const planFeatures = {
|
|
335
329
|
free: {
|
|
336
330
|
projects: 1,
|
|
337
|
-
storage: 100 * 1024 * 1024,
|
|
331
|
+
storage: 100 * 1024 * 1024, // 100MB
|
|
338
332
|
apiCalls: 100,
|
|
339
|
-
support: 'community'
|
|
333
|
+
support: 'community',
|
|
340
334
|
},
|
|
341
335
|
starter: {
|
|
342
336
|
projects: 5,
|
|
343
|
-
storage: 1 * 1024 * 1024 * 1024,
|
|
337
|
+
storage: 1 * 1024 * 1024 * 1024, // 1GB
|
|
344
338
|
apiCalls: 1000,
|
|
345
|
-
support: 'email'
|
|
339
|
+
support: 'email',
|
|
346
340
|
},
|
|
347
341
|
pro: {
|
|
348
342
|
projects: Infinity,
|
|
349
|
-
storage: 10 * 1024 * 1024 * 1024,
|
|
343
|
+
storage: 10 * 1024 * 1024 * 1024, // 10GB
|
|
350
344
|
apiCalls: 10000,
|
|
351
|
-
support: 'priority'
|
|
345
|
+
support: 'priority',
|
|
352
346
|
},
|
|
353
347
|
enterprise: {
|
|
354
348
|
projects: Infinity,
|
|
355
349
|
storage: Infinity,
|
|
356
350
|
apiCalls: Infinity,
|
|
357
|
-
support: 'dedicated'
|
|
358
|
-
}
|
|
351
|
+
support: 'dedicated',
|
|
352
|
+
},
|
|
359
353
|
}
|
|
360
354
|
|
|
361
355
|
export function getFeatureLimits(plan) {
|