Setup currently supported at macOS and Linux only.
1. Install Encore:
- macOS:
brew install encoredev/tap/encore
- Linux:
curl -L https://encore.dev/install.sh | bash
- Windows:
iwr https://encore.dev/install.ps1 | iex
2. Docker:
- Install Docker
- Start Docker
3. Configure temporal:
3a. If you have local Temporal dev server https://learn.temporal.io/getting_started/typescript/dev_environment/
The project configs assume it should be available:
- The Temporal Service be available on localhost:7233.
- The Temporal Web UI be available at http://localhost:8233.
Run the shell command at the project root dir for local Temporal to setup search attributes:
make init-temporal
3b. If you don't want to install dev Temporal Run it in docker by this command, this starts Temporal Dev server and configures search attributes:
make init-temporal-docker
3c. Alternatively, one can setup the project Temporal search parameters manually:
temporal operator search-attribute create --namespace default --name CustomerID --type Keyword
temporal operator search-attribute create --namespace default --name CustomerID --type Keyword
temporal operator search-attribute create --namespace default --name BillingPeriodNum --type Int
temporal operator search-attribute create --namespace default --name BillStatus --type Keyword
temporal operator search-attribute create --namespace default --name BillCurrency --type Keyword
temporal operator search-attribute create --namespace default --name BillItemCount --type Int
temporal operator search-attribute create --namespace default --name BillTotalCents --type Int
Download dependencies and run test (this will run 'encore run'):
make compile
Run app using command line from the root of this repository:
encore run
Create a bill (currency: GEL or USD, period YYYY-MM):
curl -sS -X POST 'http://127.0.0.1:4000/api/v1/customers/cust-1/bills' \
-H 'Content-Type: application/json' \
-d '{"currency":"USD","billingPeriod":"2025-09"}' | jq .
Add a line item (idempotent):
curl -sS -X POST 'http://127.0.0.1:4000/api/v1/customers/cust-1/bills/2025-09/items' \
-H 'Content-Type: application/json' \
-d '{"description":"api fee","amount":"2.50","IdempotencyKey":"li-1"}' | jq .
Get bill:
curl -sS 'http://127.0.0.1:4000/api/v1/customers/cust-1/bills/2025-09' | jq .
List bills for customer (filter by status and period range):
curl -sS 'http://127.0.0.1:4000/api/v1/customers/cust-1/bills?status=OPEN&from=2025-01&to=2025-12' | jq .
Close the bill:
curl -sS -X POST 'http://127.0.0.1:4000/api/v1/customers/cust-1/bills/2025-09/close' | jq .
While encore run
is running, open http://localhost:9400/ to access Encore's local developer dashboard.
Encore app name is "temporal-workflow" in the developer dashboard.
Here you can see traces for all your requests, view your architecture diagram, and see API docs in the Service Catalog.
The system follows Clean Architecture principles with clear separation of concerns:
fees/
├── app/ # Application Layer
│ ├── ports.go # Interface definitions
│ ├── usecases/ # Business logic use cases
│ ├── workflows/ # Temporal workflow definitions
│ └── views/ # Data transfer objects
├── domain/ # Domain Layer
│ ├── bill.go # Core business entities
│ └── id.go # Domain value objects
├── internal/ # Internal Infrastructure
│ ├── adapters/ # External service adapters
│ └── validation/ # Input validation utilities
└── services/ # Service Layer
├── feesapi/ # REST API service
└── worker/ # Temporal worker service
Bill
- Core aggregate representing a monthly billLineItem
- Individual fee items with idempotency supportBillID
- Domain value object for bill identification- Business Rules: State transitions, currency handling, total calculations
- Use Cases: CreateBill, AddLineItem, CloseBill, GetBill, SearchBills
- Workflows: MonthlyFeeAccrualWorkflow (Temporal orchestration)
- Ports: Interfaces for external dependencies (TemporalPort)
- DTOs: Data transfer objects for API communication
- Temporal Gateway: Adapter for Temporal workflow operations
- Validation: Input validation using go-playground/validator
- Money Library: Custom currency handling with decimal precision
- FeesAPI: RESTful API service with Encore
- Worker: Temporal worker for workflow execution
- Configuration: Environment-specific settings
The core business process is modeled as a single Temporal workflow:
MonthlyFeeAccrualWorkflow(ctx, params MonthlyFeeAccrualWorkflowParams)
Workflow Lifecycle:
- Initialization: Creates a new
domain.Bill
with OPEN status - Progressive Accrual: Accepts
SignalAddLineItem
to add fees - Closure: Accepts
SignalCloseBill
to finalize the bill - Invoice Processing: Executes activities for external invoicing
- Completion: Transitions bill to CLOSED status
Key Features:
- Idempotency: Duplicate line items are ignored based on idempotency keys
- State Management: Bill state is maintained within the workflow
- Search Attributes: Real-time visibility through Temporal search attributes
- Error Handling: Robust error handling with retry policies
- Query Support: Real-time bill state queries via
QueryState
The system uses Temporal search attributes for visibility and filtering:
Attribute | Type | Purpose |
---|---|---|
CustomerID |
Keyword | Filter bills by customer |
BillingPeriodNum |
Int | Filter by billing period (YYYYMM) |
BillStatus |
Keyword | Filter by bill status (OPEN/PENDING/CLOSED) |
BillCurrency |
Keyword | Filter by currency (USD/GEL) |
BillItemCount |
Int | Track number of line items |
BillTotalCents |
Int | Track total amount in cents |
Method | Endpoint | Description |
---|---|---|
POST |
/api/v1/customers/{customerID}/bills |
Create a new monthly bill |
POST |
/api/v1/customers/{customerID}/bills/{period}/items |
Add a line item to a bill |
POST |
/api/v1/customers/{customerID}/bills/{period}/close |
Close a bill |
GET |
/api/v1/customers/{customerID}/bills/{period} |
Get bill details |
GET |
/api/v1/customers/{customerID}/bills |
List bills with filters |
Create Bill:
POST /api/v1/customers/cust-1/bills
{
"currency": "USD",
"billingPeriod": "2025-01"
}
Add Line Item:
POST /api/v1/customers/cust-1/bills/2025-01/items
{
"description": "API usage fee",
"amount": "10.50",
"IdempotencyKey": "api-fee-2025-01-15"
}
Bill Response:
{
"id": "bill/cust-1/2025-01",
"customerId": "cust-1",
"currency": "USD",
"billingPeriod": "2025-01",
"status": "OPEN",
"items": [
{
"idempotencyKey": "api-fee-2025-01-15",
"description": "API usage fee",
"amount": {"Value": "10.50", "Currency": "USD"},
"addedAt": "2025-01-15T10:30:00Z"
}
],
"total": "10.50",
"createdAt": "2025-01-01T00:00:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}
Bill Aggregate:
type Bill struct {
ID BillID
CustomerID string
Currency libmoney.Currency
BillingPeriod BillingPeriod
Status BillStatus
Items []LineItem
Total libmoney.Money
CreatedAt time.Time
UpdatedAt time.Time
FinalizedAt *time.Time
}
Line Item:
type LineItem struct {
IdempotencyKey string
Description string
Amount libmoney.Money
AddedAt time.Time
}
The bill follows a strict state machine:
OPEN → PENDING → CLOSED
↓ ↓
ERROR ERROR
- OPEN: Bill is active and accepting line items
- PENDING: Bill is being processed (invoicing/charging)
- CLOSED: Bill is finalized and no longer accepting items
- ERROR: Bill encountered an error during processing
The system uses Encore's configuration system with environment-specific settings:
Development Configuration:
#Config: {
Temporal: {
Host: "localhost:7233"
Namespace: "default"
UseTLS: false
UseAPIKey: false
}
}