mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2026-03-10 20:51:44 +00:00
292 lines
8.5 KiB
Markdown
292 lines
8.5 KiB
Markdown
# Allowance Scheduling System (Backend)
|
|
|
|
This document captures the complete allowance scheduling feature as implemented in the backend.
|
|
It is intended as a long-term reference for how the system works, why it was built this way,
|
|
and how to operate/extend it safely.
|
|
|
|
## Goals and Scope
|
|
|
|
- Allow parents/guardians to create a recurring allowance schedule for a child.
|
|
- Credit the allowance automatically based on the schedule.
|
|
- Scale safely with multiple workers and avoid duplicate credits.
|
|
- Keep cron lightweight and offload work to RabbitMQ workers.
|
|
|
|
Non-goals in the current implementation:
|
|
- UI changes
|
|
- Analytics/reporting views
|
|
- Advanced scheduling (e.g., custom weekdays)
|
|
|
|
## High-Level Flow
|
|
|
|
1. Parent creates a schedule via API.
|
|
2. Cron runs every 5 minutes and enqueues due schedules into RabbitMQ.
|
|
3. Workers consume queue messages, credit the allowance, and update the next run.
|
|
4. Idempotency is enforced in the database to prevent duplicate credits.
|
|
|
|
## Data Model
|
|
|
|
### Table: `allowance_schedules`
|
|
|
|
Purpose: Stores the schedule definition (amount, frequency, status, next run).
|
|
|
|
Key fields:
|
|
- `guardian_id`, `junior_id`: who funds and who receives.
|
|
- `amount`: the allowance amount.
|
|
- `frequency`: DAILY / WEEKLY / MONTHLY.
|
|
- `status`: ON / OFF.
|
|
- `next_run_at`, `last_run_at`: scheduling metadata.
|
|
|
|
Constraints:
|
|
- Unique `(guardian_id, junior_id)` ensures one schedule per child.
|
|
|
|
Entity: `src/allowance/entities/allowance-schedule.entity.ts`
|
|
|
|
### Table: `allowance_credits`
|
|
|
|
Purpose: Audit log and idempotency guard for each credit run.
|
|
|
|
Key fields:
|
|
- `schedule_id`: which schedule executed.
|
|
- `transaction_id`: the resulting transaction (nullable).
|
|
- `amount`, `run_at`, `credited_at`.
|
|
|
|
Idempotency:
|
|
- Unique `(schedule_id, run_at)` prevents duplicates even with multiple workers.
|
|
|
|
Entity: `src/allowance/entities/allowance-credit.entity.ts`
|
|
|
|
### Table: `cron_runs`
|
|
|
|
Purpose: shared audit table for all cron jobs (not just allowance).
|
|
|
|
Key fields:
|
|
- `job_name`: unique identifier for the cron.
|
|
- `status`: SUCCESS / FAILED.
|
|
- `processed_count`: number of schedules processed.
|
|
- `error_message`: failure reason.
|
|
- `started_at`, `finished_at`.
|
|
|
|
Entity: `src/cron/entities/cron-run.entity.ts`
|
|
|
|
## API (Schedule Creation)
|
|
|
|
Endpoint:
|
|
- `POST /guardians/me/allowances/:juniorId`
|
|
|
|
Input DTO:
|
|
- `amount` (required, numeric, positive)
|
|
- `frequency` (required, enum)
|
|
- `status` (required, enum)
|
|
|
|
DTO: `src/allowance/dtos/request/create-allowance-schedule.request.dto.ts`
|
|
|
|
Response DTO:
|
|
- `AllowanceScheduleResponseDto` with schedule fields.
|
|
|
|
DTO: `src/allowance/dtos/response/allowance-schedule.response.dto.ts`
|
|
|
|
Business validation:
|
|
- Child must belong to guardian.
|
|
- No duplicate schedule for the same guardian+child.
|
|
|
|
Service: `src/allowance/services/allowance.service.ts`
|
|
|
|
## Cron Producer (Queue Enqueue)
|
|
|
|
Cron job:
|
|
- Runs every 5 minutes.
|
|
- Batches schedules (cursor-based) to avoid large load.
|
|
- Enqueues each schedule to RabbitMQ.
|
|
|
|
Cron: `src/cron/tasks/allowance-schedule.cron.ts`
|
|
|
|
Batch behavior:
|
|
- Batch size = 100
|
|
- Uses a cursor (`nextRunAt`, `id`) for stable pagination.
|
|
- Prevents re-reading the same rows.
|
|
|
|
Locking:
|
|
- `BaseCronService` uses cache lock to ensure only one instance runs.
|
|
- Each cron run is logged to `cron_runs` with status and processed count.
|
|
|
|
Lock: `src/cron/services/base-cron.service.ts`
|
|
|
|
## Queue Publisher
|
|
|
|
Queue publisher:
|
|
- Asserts queue + retry/DLQ exchanges.
|
|
- Enqueues jobs with message id (for traceability).
|
|
|
|
Service: `src/allowance/services/allowance-queue.service.ts`
|
|
|
|
Queue names:
|
|
- `allowance.schedule` (main)
|
|
- `allowance.schedule.retry` (retry with TTL)
|
|
- `allowance.schedule.dlq` (dead-letter queue)
|
|
|
|
Constants: `src/allowance/constants/allowance-queue.constants.ts`
|
|
|
|
## Worker Consumer (Transfers)
|
|
|
|
Worker:
|
|
- Consumes `allowance.schedule` queue.
|
|
- Validates schedule is due and active.
|
|
- Creates `allowance_credits` record for idempotency.
|
|
- Transfers money via `cardService.transferToChild`.
|
|
- Updates `last_run_at` and `next_run_at`.
|
|
|
|
Worker: `src/allowance/services/allowance-worker.service.ts`
|
|
|
|
Transfer:
|
|
- Uses existing logic in `card.service.ts` for balance updates and transaction creation.
|
|
|
|
Service: `src/card/services/card.service.ts`
|
|
|
|
### Idempotency details
|
|
|
|
1. Worker inserts `allowance_credits` row first.
|
|
2. Unique constraint blocks duplicates.
|
|
3. If transfer fails, the credit row is removed so the job can retry.
|
|
|
|
This makes multiple workers safe.
|
|
|
|
## Retry + DLQ Strategy
|
|
|
|
Retry delay:
|
|
- Failed jobs are dead-lettered to retry queue.
|
|
- Retry queue has `messageTtl = 10 minutes`.
|
|
- After TTL, job is routed back to main queue.
|
|
|
|
DLQ:
|
|
- If a job fails `ALLOWANCE_MAX_RETRIES` times (default 5), it is routed to the DLQ.
|
|
- This prevents endless loops and allows manual inspection.
|
|
|
|
Config:
|
|
- `ALLOWANCE_MAX_RETRIES` (default 5)
|
|
|
|
## Redis Usage
|
|
|
|
Redis is used by `BaseCronService` to enforce a **distributed lock** for the cron job.
|
|
This prevents multiple backend instances from enqueuing the same schedules at the same time.
|
|
|
|
Lock behavior:
|
|
- If the lock key exists, cron exits early.
|
|
- If the lock key is absent, cron sets it with a TTL and proceeds.
|
|
- TTL ensures the lock is released even if a node crashes.
|
|
|
|
Service: `src/cron/services/base-cron.service.ts`
|
|
|
|
## RabbitMQ Setup and Behavior
|
|
|
|
The allowance system uses RabbitMQ for asynchronous processing. Cron publishes due schedules,
|
|
and workers consume them.
|
|
|
|
### Exchanges and Queues
|
|
|
|
Main queue:
|
|
- `allowance.schedule`
|
|
|
|
Retry setup:
|
|
- Exchange: `allowance.schedule.retry.exchange`
|
|
- Queue: `allowance.schedule.retry`
|
|
- Binding key: `allowance.schedule`
|
|
- TTL: 10 minutes
|
|
- Dead-letter route: back to `allowance.schedule`
|
|
|
|
Dead-letter queue (DLQ):
|
|
- Exchange: `allowance.schedule.dlq.exchange`
|
|
- Queue: `allowance.schedule.dlq`
|
|
- Binding key: `allowance.schedule`
|
|
|
|
### Flow Summary
|
|
|
|
1. Cron enqueues jobs to `allowance.schedule`.
|
|
2. Worker consumes jobs from `allowance.schedule`.
|
|
3. On failure, job is **dead-lettered** to `allowance.schedule.retry`.
|
|
4. After 10 minutes, it returns to `allowance.schedule`.
|
|
5. After max retries, worker publishes the job to `allowance.schedule.dlq`.
|
|
|
|
### Where it is configured
|
|
|
|
- Publisher setup: `src/allowance/services/allowance-queue.service.ts`
|
|
- Worker consumer: `src/allowance/services/allowance-worker.service.ts`
|
|
- Queue constants: `src/allowance/constants/allowance-queue.constants.ts`
|
|
|
|
## Environment Variables
|
|
|
|
- `RABBITMQ_URL` (required for queue/worker)
|
|
- `ALLOWANCE_QUEUE_NAME` (optional, defaults to `allowance.schedule`)
|
|
- `ALLOWANCE_MAX_RETRIES` (optional, defaults to 5)
|
|
- `ALLOWANCE_RETRY_DELAY_MS` (optional, defaults to 10 minutes)
|
|
|
|
### Example .env snippet
|
|
|
|
```
|
|
RABBITMQ_URL=amqp://guest:guest@localhost:5672
|
|
ALLOWANCE_QUEUE_NAME=allowance.schedule
|
|
ALLOWANCE_MAX_RETRIES=5
|
|
ALLOWANCE_RETRY_DELAY_MS=600000
|
|
```
|
|
|
|
## Operational Checklist
|
|
|
|
- Ensure Redis is running (cron locking).
|
|
- Ensure RabbitMQ is running (queue + workers).
|
|
- Start at least one worker process.
|
|
- Monitor DLQ for failures.
|
|
|
|
## Manual Test Checklist
|
|
|
|
1. Create schedule:
|
|
- POST `/guardians/me/allowances/:juniorId`
|
|
- Valid amount, frequency, status.
|
|
2. Duplicate schedule:
|
|
- Expect `ALLOWANCE.ALREADY_EXISTS`.
|
|
3. Cron enqueue:
|
|
- Wait for cron interval or manually trigger.
|
|
- Confirm messages appear in RabbitMQ.
|
|
4. Worker:
|
|
- Ensure worker is running.
|
|
- Verify transfers happen and `allowance_credits` is created.
|
|
5. Failure paths:
|
|
- Simulate transfer failure and verify retry queue behavior.
|
|
- Confirm DLQ after max retries.
|
|
|
|
## Operational Notes
|
|
|
|
- For large volumes, scale workers horizontally.
|
|
- Keep cron lightweight; do not perform transfers in cron.
|
|
- Monitor queue depth and DLQ entries.
|
|
|
|
## Known Limitations (Current)
|
|
|
|
- Only one schedule per child (guardian+junior unique).
|
|
- No custom weekdays or complex schedules.
|
|
- Retry delay is fixed at 10 minutes (can be configurable).
|
|
|
|
## File Map (Quick Reference)
|
|
|
|
- API:
|
|
- `src/allowance/controllers/allowance.controller.ts`
|
|
- `src/allowance/services/allowance.service.ts`
|
|
- `src/allowance/dtos/request/create-allowance-schedule.request.dto.ts`
|
|
- `src/allowance/dtos/response/allowance-schedule.response.dto.ts`
|
|
|
|
- Cron:
|
|
- `src/cron/tasks/allowance-schedule.cron.ts`
|
|
- `src/cron/services/base-cron.service.ts`
|
|
|
|
- Queue/Worker:
|
|
- `src/allowance/services/allowance-queue.service.ts`
|
|
- `src/allowance/services/allowance-worker.service.ts`
|
|
- `src/allowance/constants/allowance-queue.constants.ts`
|
|
|
|
- Repositories:
|
|
- `src/allowance/repositories/allowance-schedule.repository.ts`
|
|
- `src/allowance/repositories/allowance-credit.repository.ts`
|
|
|
|
- Entities:
|
|
- `src/allowance/entities/allowance-schedule.entity.ts`
|
|
- `src/allowance/entities/allowance-credit.entity.ts`
|
|
|