facera 0.1.0
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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.github/workflows/gem-push.yml +42 -0
- data/.github/workflows/ruby.yml +35 -0
- data/.gitignore +39 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +137 -0
- data/Gemfile +9 -0
- data/LICENSE +21 -0
- data/README.md +309 -0
- data/Rakefile +6 -0
- data/examples/01_core_dsl.rb +132 -0
- data/examples/02_facet_system.rb +216 -0
- data/examples/03_api_generation.rb +117 -0
- data/examples/04_auto_mounting.rb +182 -0
- data/examples/05_adapters.rb +196 -0
- data/examples/README.md +184 -0
- data/examples/server/README.md +376 -0
- data/examples/server/adapters/payment_adapter.rb +139 -0
- data/examples/server/application.rb +17 -0
- data/examples/server/config/facera.rb +33 -0
- data/examples/server/config.ru +10 -0
- data/examples/server/cores/payment_core.rb +82 -0
- data/examples/server/facets/external_facet.rb +38 -0
- data/examples/server/facets/internal_facet.rb +33 -0
- data/examples/server/facets/operator_facet.rb +48 -0
- data/facera.gemspec +30 -0
- data/img/facera.png +0 -0
- data/lib/facera/adapter.rb +83 -0
- data/lib/facera/attribute.rb +95 -0
- data/lib/facera/auto_mount.rb +124 -0
- data/lib/facera/capability.rb +117 -0
- data/lib/facera/capability_access.rb +59 -0
- data/lib/facera/configuration.rb +83 -0
- data/lib/facera/context.rb +29 -0
- data/lib/facera/core.rb +65 -0
- data/lib/facera/dsl.rb +41 -0
- data/lib/facera/entity.rb +50 -0
- data/lib/facera/error_formatter.rb +100 -0
- data/lib/facera/errors.rb +40 -0
- data/lib/facera/executor.rb +265 -0
- data/lib/facera/facet.rb +103 -0
- data/lib/facera/field_visibility.rb +69 -0
- data/lib/facera/generators/core_generator.rb +23 -0
- data/lib/facera/generators/facet_generator.rb +25 -0
- data/lib/facera/generators/install_generator.rb +64 -0
- data/lib/facera/generators/templates/core.rb.tt +49 -0
- data/lib/facera/generators/templates/facet.rb.tt +23 -0
- data/lib/facera/grape/api_generator.rb +59 -0
- data/lib/facera/grape/endpoint_generator.rb +316 -0
- data/lib/facera/grape/entity_generator.rb +89 -0
- data/lib/facera/grape.rb +14 -0
- data/lib/facera/introspection.rb +111 -0
- data/lib/facera/introspection_api.rb +66 -0
- data/lib/facera/invariant.rb +26 -0
- data/lib/facera/loader.rb +153 -0
- data/lib/facera/openapi_generator.rb +338 -0
- data/lib/facera/railtie.rb +51 -0
- data/lib/facera/registry.rb +34 -0
- data/lib/facera/tasks/routes.rake +66 -0
- data/lib/facera/version.rb +3 -0
- data/lib/facera.rb +35 -0
- metadata +137 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# Facera Payment API Server
|
|
2
|
+
|
|
3
|
+
A real-world example of a multi-facet API using Facera's auto-mounting feature.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
server/
|
|
9
|
+
├── config.ru # Rack server (just 15 lines!)
|
|
10
|
+
├── application.rb # Application builder (34 lines)
|
|
11
|
+
├── config/
|
|
12
|
+
│ └── facera.rb # Facera configuration (33 lines)
|
|
13
|
+
├── cores/
|
|
14
|
+
│ └── payment_core.rb # Payment domain model
|
|
15
|
+
└── facets/
|
|
16
|
+
├── external_facet.rb # Public API
|
|
17
|
+
├── internal_facet.rb # Service-to-service API
|
|
18
|
+
└── operator_facet.rb # Admin/support API
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Clean separation:**
|
|
22
|
+
- `config.ru` - Just loads and runs (15 lines)
|
|
23
|
+
- `application.rb` - Builds the Rack app
|
|
24
|
+
- `config/facera.rb` - Facera configuration
|
|
25
|
+
- `cores/` and `facets/` - Auto-discovered!
|
|
26
|
+
- No manual requires needed!
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Start the server
|
|
32
|
+
rackup -p 9292
|
|
33
|
+
|
|
34
|
+
# Or with specific environment
|
|
35
|
+
RACK_ENV=development rackup -p 9292
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The server automatically:
|
|
39
|
+
1. Loads configuration from `config/facera.rb`
|
|
40
|
+
2. Discovers all cores in `cores/`
|
|
41
|
+
3. Discovers all facets in `facets/`
|
|
42
|
+
4. Mounts APIs at configured paths
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
All Facera configuration is in `config/facera.rb`:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
Facera.configure do |config|
|
|
50
|
+
config.base_path = '/api'
|
|
51
|
+
config.version = 'v1'
|
|
52
|
+
|
|
53
|
+
# Custom paths
|
|
54
|
+
config.facet_path :external, '/v1'
|
|
55
|
+
config.facet_path :internal, '/internal/v1'
|
|
56
|
+
|
|
57
|
+
# Conditional features
|
|
58
|
+
config.disable_facet :operator unless ENV['ENABLE_OPERATOR_API']
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Environment-Based Configuration
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Disable operator API in production
|
|
66
|
+
ENABLE_OPERATOR_API=false rackup -p 9292
|
|
67
|
+
|
|
68
|
+
# Custom port
|
|
69
|
+
rackup -p 8080
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## API Facets
|
|
73
|
+
|
|
74
|
+
### External API (Public)
|
|
75
|
+
|
|
76
|
+
**Path:** `/api/v1`
|
|
77
|
+
**Audience:** External clients, mobile apps, web browsers
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Health check
|
|
81
|
+
curl http://localhost:9292/api/v1/health
|
|
82
|
+
|
|
83
|
+
# Create payment
|
|
84
|
+
curl -X POST http://localhost:9292/api/v1/payments \
|
|
85
|
+
-H 'Content-Type: application/json' \
|
|
86
|
+
-d '{
|
|
87
|
+
"amount": 100.0,
|
|
88
|
+
"currency": "USD",
|
|
89
|
+
"merchant_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
90
|
+
"customer_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
|
|
91
|
+
}'
|
|
92
|
+
|
|
93
|
+
# Get payment (limited fields)
|
|
94
|
+
curl http://localhost:9292/api/v1/payments/{id}
|
|
95
|
+
|
|
96
|
+
# List payments
|
|
97
|
+
curl http://localhost:9292/api/v1/payments?limit=10
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Field Visibility:** 6/11 fields (secure)
|
|
101
|
+
**Capabilities:** Create, Read, List only
|
|
102
|
+
**Error Detail:** Minimal
|
|
103
|
+
|
|
104
|
+
### Internal API (Service-to-Service)
|
|
105
|
+
|
|
106
|
+
**Path:** `/api/internal/v1`
|
|
107
|
+
**Audience:** Internal microservices
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Health check
|
|
111
|
+
curl http://localhost:9292/api/internal/v1/health
|
|
112
|
+
|
|
113
|
+
# Get payment (all fields + computed)
|
|
114
|
+
curl http://localhost:9292/api/internal/v1/payments/{id}
|
|
115
|
+
|
|
116
|
+
# Confirm payment (internal only)
|
|
117
|
+
curl -X POST http://localhost:9292/api/internal/v1/payments/{id}/confirm
|
|
118
|
+
|
|
119
|
+
# Cancel payment (internal only)
|
|
120
|
+
curl -X POST http://localhost:9292/api/internal/v1/payments/{id}/cancel
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Field Visibility:** All fields + computed
|
|
124
|
+
**Capabilities:** Full access
|
|
125
|
+
**Error Detail:** Detailed
|
|
126
|
+
|
|
127
|
+
### Operator API (Admin/Support)
|
|
128
|
+
|
|
129
|
+
**Path:** `/api/operator/v1`
|
|
130
|
+
**Audience:** Support staff, admin tools
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Health check
|
|
134
|
+
curl http://localhost:9292/api/operator/v1/health
|
|
135
|
+
|
|
136
|
+
# Get payment with operator fields
|
|
137
|
+
curl http://localhost:9292/api/operator/v1/payments/{id}
|
|
138
|
+
# Includes: customer_name, merchant_name, time_in_current_state
|
|
139
|
+
|
|
140
|
+
# Full operation access
|
|
141
|
+
curl -X POST http://localhost:9292/api/operator/v1/payments/{id}/confirm
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Field Visibility:** All fields + admin computed
|
|
145
|
+
**Capabilities:** Full access
|
|
146
|
+
**Error Detail:** Detailed + structured
|
|
147
|
+
|
|
148
|
+
## Adding a New Facet
|
|
149
|
+
|
|
150
|
+
1. **Create the facet file** in `facets/`:
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# facets/partner_facet.rb
|
|
154
|
+
Facera.define_facet(:partner, core: :payment) do
|
|
155
|
+
description "Partner integration API"
|
|
156
|
+
|
|
157
|
+
expose :payment do
|
|
158
|
+
fields :id, :amount, :status
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
allow_capabilities :get_payment, :list_payments
|
|
162
|
+
error_verbosity :minimal
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
2. **Restart server** - that's it!
|
|
167
|
+
|
|
168
|
+
The new facet is **automatically**:
|
|
169
|
+
- ✓ Discovered from `facets/` directory
|
|
170
|
+
- ✓ Loaded into the registry
|
|
171
|
+
- ✓ Mounted at `/api/partner/v1`
|
|
172
|
+
- ✓ Documented in startup logs
|
|
173
|
+
|
|
174
|
+
No manual loading, no config changes needed!
|
|
175
|
+
|
|
176
|
+
(Optional) Configure custom path in `config/facera.rb`:
|
|
177
|
+
```ruby
|
|
178
|
+
config.facet_path :partner, '/partners/v1'
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Key Files Explained
|
|
182
|
+
|
|
183
|
+
### `config.ru` (15 lines)
|
|
184
|
+
|
|
185
|
+
Super simple - just loads the app and runs it:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
require_relative '../../lib/facera'
|
|
189
|
+
require_relative 'config/facera'
|
|
190
|
+
require_relative 'application'
|
|
191
|
+
|
|
192
|
+
run PaymentAPI::Application.build
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `application.rb` (34 lines)
|
|
196
|
+
|
|
197
|
+
Builds the Rack application with middleware and auto-mounted facets:
|
|
198
|
+
1. Middleware (Reloader, Logger)
|
|
199
|
+
2. Auto-mount all facets
|
|
200
|
+
3. Root endpoint with API info
|
|
201
|
+
|
|
202
|
+
### `config/facera.rb`
|
|
203
|
+
|
|
204
|
+
Single source of truth for Facera configuration.
|
|
205
|
+
|
|
206
|
+
## Deployment
|
|
207
|
+
|
|
208
|
+
This is a standard Rack application. Works with:
|
|
209
|
+
|
|
210
|
+
### Puma (recommended)
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
# config/puma.rb
|
|
214
|
+
workers ENV.fetch('WEB_CONCURRENCY', 2)
|
|
215
|
+
threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
|
|
216
|
+
threads threads_count, threads_count
|
|
217
|
+
|
|
218
|
+
port ENV.fetch('PORT', 9292)
|
|
219
|
+
environment ENV.fetch('RACK_ENV', 'development')
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
puma -C config/puma.rb
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Unicorn
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
# config/unicorn.rb
|
|
230
|
+
worker_processes 4
|
|
231
|
+
listen 9292
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
unicorn -c config/unicorn.rb
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Docker
|
|
239
|
+
|
|
240
|
+
```dockerfile
|
|
241
|
+
FROM ruby:3.2
|
|
242
|
+
|
|
243
|
+
WORKDIR /app
|
|
244
|
+
COPY Gemfile* ./
|
|
245
|
+
RUN bundle install
|
|
246
|
+
|
|
247
|
+
COPY . .
|
|
248
|
+
|
|
249
|
+
EXPOSE 9292
|
|
250
|
+
CMD ["rackup", "-o", "0.0.0.0", "-p", "9292"]
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Production Considerations
|
|
254
|
+
|
|
255
|
+
### 1. Authentication
|
|
256
|
+
|
|
257
|
+
Add authentication handlers in `config/facera.rb`:
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
Facera.configure do |config|
|
|
261
|
+
config.authenticate :external do |request|
|
|
262
|
+
token = request.headers['Authorization']&.sub(/^Bearer /, '')
|
|
263
|
+
User.find_by_token(token) or raise Facera::UnauthorizedError
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
config.authenticate :internal do |request|
|
|
267
|
+
service_token = request.headers['X-Service-Token']
|
|
268
|
+
Service.verify_token(service_token) or raise Facera::UnauthorizedError
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### 2. Rate Limiting
|
|
274
|
+
|
|
275
|
+
Already configured in `facets/external_facet.rb`:
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
rate_limit requests: 1000, per: :hour
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### 3. Monitoring
|
|
282
|
+
|
|
283
|
+
Enable audit logging (already on for internal/operator):
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
audit_all_operations user: :current_user
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### 4. Environment Variables
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
# Required
|
|
293
|
+
DATABASE_URL=postgresql://...
|
|
294
|
+
REDIS_URL=redis://...
|
|
295
|
+
|
|
296
|
+
# Optional
|
|
297
|
+
ENABLE_OPERATOR_API=true
|
|
298
|
+
RACK_ENV=production
|
|
299
|
+
PORT=9292
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Architecture Benefits
|
|
303
|
+
|
|
304
|
+
### Before Facera
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
3 separate APIs × 5 endpoints = 15 implementations
|
|
308
|
+
+ 3 serializers
|
|
309
|
+
+ 3 authentication systems
|
|
310
|
+
+ 3 sets of tests
|
|
311
|
+
= ~2000 lines of code
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### With Facera
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
1 core definition
|
|
318
|
+
+ 3 facet files
|
|
319
|
+
+ 1 config file
|
|
320
|
+
= ~300 lines of code
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**85% less code, 100% consistency guaranteed**
|
|
324
|
+
|
|
325
|
+
## Development Workflow
|
|
326
|
+
|
|
327
|
+
1. **Define domain model** in `cores/payment_core.rb`
|
|
328
|
+
2. **Create facet projections** in `facets/`
|
|
329
|
+
3. **Configure paths** in `config/facera.rb`
|
|
330
|
+
4. **Run** - APIs are auto-generated!
|
|
331
|
+
|
|
332
|
+
No route definitions, no serializer boilerplate, no duplication.
|
|
333
|
+
|
|
334
|
+
## Testing
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
# All facets respond
|
|
338
|
+
curl http://localhost:9292/api/v1/health
|
|
339
|
+
curl http://localhost:9292/api/internal/v1/health
|
|
340
|
+
curl http://localhost:9292/api/operator/v1/health
|
|
341
|
+
|
|
342
|
+
# Field visibility working
|
|
343
|
+
curl http://localhost:9292/api/v1/payments/{id}
|
|
344
|
+
# Returns: id, amount, currency, status (6 fields)
|
|
345
|
+
|
|
346
|
+
curl http://localhost:9292/api/internal/v1/payments/{id}
|
|
347
|
+
# Returns: all 11 fields + computed
|
|
348
|
+
|
|
349
|
+
# Access control working
|
|
350
|
+
curl -X POST http://localhost:9292/api/v1/payments/{id}/confirm
|
|
351
|
+
# 404 - not available in external facet
|
|
352
|
+
|
|
353
|
+
curl -X POST http://localhost:9292/api/internal/v1/payments/{id}/confirm
|
|
354
|
+
# 200 - available in internal facet
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Troubleshooting
|
|
358
|
+
|
|
359
|
+
### Facet not mounting?
|
|
360
|
+
|
|
361
|
+
Check auto-mount logs:
|
|
362
|
+
```
|
|
363
|
+
I, INFO -- : ✓ external → /api/v1 (4 endpoints)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Wrong path?
|
|
367
|
+
|
|
368
|
+
Check `config/facera.rb` paths.
|
|
369
|
+
|
|
370
|
+
### Missing capabilities?
|
|
371
|
+
|
|
372
|
+
Check facet's `allow_capabilities` / `deny_capabilities`.
|
|
373
|
+
|
|
374
|
+
### Field not showing?
|
|
375
|
+
|
|
376
|
+
Check facet's `expose` block and field visibility rules.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Payment Adapter
|
|
2
|
+
# Implements business logic for payment core capabilities
|
|
3
|
+
#
|
|
4
|
+
# This adapter uses in-memory storage for demo purposes.
|
|
5
|
+
# In production, you would use ActiveRecord, Sequel, or your preferred ORM.
|
|
6
|
+
|
|
7
|
+
class PaymentAdapter
|
|
8
|
+
include Facera::Adapter
|
|
9
|
+
|
|
10
|
+
# In-memory storage (for demo purposes)
|
|
11
|
+
@@payments = {}
|
|
12
|
+
|
|
13
|
+
def create_payment(params)
|
|
14
|
+
payment = {
|
|
15
|
+
id: SecureRandom.uuid,
|
|
16
|
+
amount: params[:amount],
|
|
17
|
+
currency: params[:currency],
|
|
18
|
+
merchant_id: params[:merchant_id],
|
|
19
|
+
customer_id: params[:customer_id],
|
|
20
|
+
status: :pending,
|
|
21
|
+
created_at: Time.now
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@@payments[payment[:id]] = payment
|
|
25
|
+
|
|
26
|
+
# In production, you might:
|
|
27
|
+
# - Save to database: Payment.create!(params)
|
|
28
|
+
# - Validate with external service
|
|
29
|
+
# - Send notification
|
|
30
|
+
# - Log audit trail
|
|
31
|
+
|
|
32
|
+
payment
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def get_payment(params)
|
|
36
|
+
payment = @@payments[params[:id]]
|
|
37
|
+
|
|
38
|
+
raise Facera::NotFoundError, "Payment not found" unless payment
|
|
39
|
+
|
|
40
|
+
payment
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def list_payments(params)
|
|
44
|
+
payments = @@payments.values
|
|
45
|
+
|
|
46
|
+
# Apply filters
|
|
47
|
+
if params[:merchant_id]
|
|
48
|
+
payments = payments.select { |p| p[:merchant_id] == params[:merchant_id] }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if params[:customer_id]
|
|
52
|
+
payments = payments.select { |p| p[:customer_id] == params[:customer_id] }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if params[:status]
|
|
56
|
+
payments = payments.select { |p| p[:status].to_s == params[:status].to_s }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Apply pagination
|
|
60
|
+
limit = (params[:limit] || 20).to_i
|
|
61
|
+
offset = (params[:offset] || 0).to_i
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
data: payments[offset, limit] || [],
|
|
65
|
+
meta: {
|
|
66
|
+
total: payments.count,
|
|
67
|
+
limit: limit,
|
|
68
|
+
offset: offset
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def confirm_payment(params)
|
|
74
|
+
payment = get_payment(params)
|
|
75
|
+
|
|
76
|
+
# Validate precondition (already done by framework, but good practice)
|
|
77
|
+
if payment[:status] != :pending
|
|
78
|
+
raise Facera::PreconditionError, "Payment must be pending to confirm"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Update payment
|
|
82
|
+
payment[:status] = :confirmed
|
|
83
|
+
payment[:confirmed_at] = Time.now
|
|
84
|
+
|
|
85
|
+
# In production, you might:
|
|
86
|
+
# - Call payment gateway API
|
|
87
|
+
# - Send confirmation email: PaymentMailer.confirmation(payment).deliver_later
|
|
88
|
+
# - Publish event: PaymentEvents.publish(:payment_confirmed, payment)
|
|
89
|
+
# - Update accounting system
|
|
90
|
+
# - Trigger fulfillment workflow
|
|
91
|
+
|
|
92
|
+
payment
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def cancel_payment(params)
|
|
96
|
+
payment = get_payment(params)
|
|
97
|
+
|
|
98
|
+
if payment[:status] != :pending
|
|
99
|
+
raise Facera::PreconditionError, "Payment must be pending to cancel"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
payment[:status] = :cancelled
|
|
103
|
+
payment[:cancelled_at] = Time.now
|
|
104
|
+
|
|
105
|
+
# In production:
|
|
106
|
+
# - Refund if already charged
|
|
107
|
+
# - Send cancellation email
|
|
108
|
+
# - Log cancellation reason
|
|
109
|
+
# - Update metrics
|
|
110
|
+
|
|
111
|
+
payment
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def refund_payment(params)
|
|
115
|
+
payment = get_payment(params)
|
|
116
|
+
|
|
117
|
+
if payment[:status] != :confirmed
|
|
118
|
+
raise Facera::PreconditionError, "Payment must be confirmed to refund"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
refund_amount = params[:refund_amount] || payment[:amount]
|
|
122
|
+
|
|
123
|
+
if refund_amount > payment[:amount]
|
|
124
|
+
raise Facera::ValidationError, "Refund amount cannot exceed payment amount"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
payment[:status] = :refunded
|
|
128
|
+
payment[:refunded_at] = Time.now
|
|
129
|
+
payment[:refund_amount] = refund_amount
|
|
130
|
+
|
|
131
|
+
# In production:
|
|
132
|
+
# - Process refund with payment gateway
|
|
133
|
+
# - Send refund confirmation
|
|
134
|
+
# - Update accounting
|
|
135
|
+
# - Notify merchant
|
|
136
|
+
|
|
137
|
+
payment
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Application Builder
|
|
2
|
+
# Constructs the Rack application with middleware and auto-mounted facets
|
|
3
|
+
|
|
4
|
+
module PaymentAPI
|
|
5
|
+
class Application
|
|
6
|
+
def self.build
|
|
7
|
+
Rack::Builder.new do
|
|
8
|
+
# Middleware
|
|
9
|
+
use Rack::Reloader, 0 if ENV['RACK_ENV'] == 'development'
|
|
10
|
+
use Rack::CommonLogger
|
|
11
|
+
|
|
12
|
+
# Auto-mount all Facera facets and introspection API
|
|
13
|
+
Facera.auto_mount!(self)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Facera Initializer
|
|
2
|
+
# Configure Facera behavior and facet paths
|
|
3
|
+
|
|
4
|
+
Facera.configure do |config|
|
|
5
|
+
# Base path for all APIs
|
|
6
|
+
config.base_path = '/api'
|
|
7
|
+
|
|
8
|
+
# API version
|
|
9
|
+
config.version = 'v1'
|
|
10
|
+
|
|
11
|
+
# Custom paths for different facets
|
|
12
|
+
config.facet_path :external, '/v1' # /api/v1
|
|
13
|
+
config.facet_path :internal, '/internal/v1' # /api/internal/v1
|
|
14
|
+
config.facet_path :operator, '/operator/v1' # /api/operator/v1
|
|
15
|
+
|
|
16
|
+
# Enable/disable features
|
|
17
|
+
config.dashboard = false # Not implemented yet
|
|
18
|
+
config.generate_docs = true
|
|
19
|
+
|
|
20
|
+
# Disable facets based on environment
|
|
21
|
+
# config.disable_facet :operator unless ENV['ENABLE_OPERATOR_API']
|
|
22
|
+
|
|
23
|
+
# Authentication handlers (example)
|
|
24
|
+
# config.authenticate :external do |request|
|
|
25
|
+
# token = request.headers['Authorization']&.sub(/^Bearer /, '')
|
|
26
|
+
# User.find_by_token(token)
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# config.authenticate :internal do |request|
|
|
30
|
+
# service_token = request.headers['X-Service-Token']
|
|
31
|
+
# Service.verify_token(service_token)
|
|
32
|
+
# end
|
|
33
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Payment Core
|
|
2
|
+
# Defines the semantic model for payment operations
|
|
3
|
+
|
|
4
|
+
Facera.define_core(:payment) do
|
|
5
|
+
# Payment entity with all attributes
|
|
6
|
+
entity :payment do
|
|
7
|
+
# Immutable identifiers
|
|
8
|
+
attribute :id, :uuid, immutable: true
|
|
9
|
+
attribute :created_at, :timestamp, immutable: true
|
|
10
|
+
|
|
11
|
+
# Required payment information
|
|
12
|
+
attribute :amount, :money, required: true
|
|
13
|
+
attribute :currency, :string, required: true
|
|
14
|
+
attribute :merchant_id, :uuid, required: true
|
|
15
|
+
attribute :customer_id, :uuid, required: true
|
|
16
|
+
|
|
17
|
+
# Optional information
|
|
18
|
+
attribute :description, :string
|
|
19
|
+
attribute :metadata, :hash
|
|
20
|
+
|
|
21
|
+
# State tracking
|
|
22
|
+
attribute :status, :enum, values: [:pending, :confirmed, :cancelled]
|
|
23
|
+
attribute :confirmed_at, :timestamp
|
|
24
|
+
attribute :cancelled_at, :timestamp
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Business invariants
|
|
28
|
+
invariant :positive_amount, description: "Payment amount must be positive" do
|
|
29
|
+
amount.nil? || amount > 0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
invariant :valid_status_transitions, description: "Only valid state transitions allowed" do
|
|
33
|
+
# This would check against actual previous state in a real implementation
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Create a new payment
|
|
38
|
+
capability :create_payment, type: :create do
|
|
39
|
+
entity :payment
|
|
40
|
+
requires :amount, :currency, :merchant_id, :customer_id
|
|
41
|
+
optional :description, :metadata
|
|
42
|
+
|
|
43
|
+
validates do
|
|
44
|
+
amount > 0
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Retrieve a specific payment
|
|
49
|
+
capability :get_payment, type: :get do
|
|
50
|
+
entity :payment
|
|
51
|
+
requires :id
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# List payments with optional filters
|
|
55
|
+
capability :list_payments, type: :list do
|
|
56
|
+
entity :payment
|
|
57
|
+
optional :limit, :offset, :merchant_id, :customer_id, :status
|
|
58
|
+
filterable :merchant_id, :customer_id, :status
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Confirm a pending payment
|
|
62
|
+
capability :confirm_payment, type: :action do
|
|
63
|
+
entity :payment
|
|
64
|
+
requires :id
|
|
65
|
+
optional :confirmation_code
|
|
66
|
+
|
|
67
|
+
precondition { status == :pending }
|
|
68
|
+
transitions_to :confirmed
|
|
69
|
+
sets confirmed_at: -> { Time.now }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Cancel a pending payment
|
|
73
|
+
capability :cancel_payment, type: :action do
|
|
74
|
+
entity :payment
|
|
75
|
+
requires :id
|
|
76
|
+
optional :reason
|
|
77
|
+
|
|
78
|
+
precondition { status == :pending }
|
|
79
|
+
transitions_to :cancelled
|
|
80
|
+
sets cancelled_at: -> { Time.now }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# External Facet
|
|
2
|
+
# Public-facing API for external clients
|
|
3
|
+
# Exposes limited fields and capabilities for security
|
|
4
|
+
|
|
5
|
+
Facera.define_facet(:external, core: :payment) do
|
|
6
|
+
description "Public API for external clients"
|
|
7
|
+
|
|
8
|
+
# Expose only safe fields to external consumers
|
|
9
|
+
expose :payment do
|
|
10
|
+
fields :id, :amount, :currency, :status, :description, :created_at
|
|
11
|
+
|
|
12
|
+
# Hide sensitive internal fields
|
|
13
|
+
hide :merchant_id, :customer_id, :metadata, :confirmed_at, :cancelled_at
|
|
14
|
+
|
|
15
|
+
# Use camelCase for external API (common in JavaScript/TypeScript clients)
|
|
16
|
+
alias_field :created_at, as: :createdAt
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Limit capabilities to read operations and creation
|
|
20
|
+
allow_capabilities :create_payment, :get_payment, :list_payments
|
|
21
|
+
|
|
22
|
+
# Explicitly deny admin operations
|
|
23
|
+
deny_capabilities :confirm_payment, :cancel_payment
|
|
24
|
+
|
|
25
|
+
# Scope list operations to current customer
|
|
26
|
+
# In a real app, this would use authentication context
|
|
27
|
+
scope :list_payments do
|
|
28
|
+
# This would be: { customer_id: current_user.id }
|
|
29
|
+
# For demo purposes, we'll just pass through
|
|
30
|
+
{}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Minimal error messages for security
|
|
34
|
+
error_verbosity :minimal
|
|
35
|
+
|
|
36
|
+
# Optional: Rate limiting configuration
|
|
37
|
+
rate_limit requests: 1000, per: :hour
|
|
38
|
+
end
|