create the specf document

This commit is contained in:
saqib mir
2026-09-09 17:39:12 +05:30
parent 0e3543bb01
commit 794e16f349
3 changed files with 72 additions and 63 deletions
+23 -58
View File
@@ -1,66 +1,31 @@
# SupportHub API
### Development
- docker compose --env-file .env.development -f docker-compose.development.yml up -d --build
### Development (Docker)
- Start all services: `docker compose --env-file .env.development -f docker-compose.development.yml up -d --build`
- Start only database & cache (for local app development): `docker compose --env-file .env.development -f docker-compose.development.yml up -d postgres redis`
### Test
- docker compose --env-file .env.test -f docker-compose.test.yml up --build
### Test (Docker)
- `docker compose --env-file .env.test -f docker-compose.test.yml up --build`
### Production
- docker compose --env-file .env.prod -f docker-compose.prod.yml up --build -d
### Production (Docker)
- `docker compose --env-file .env.prod -f docker-compose.prod.yml up --build -d`
### Stop
- docker compose -f docker-compose.prod.yml down
### Stop / Down
- Stop production: `docker compose -f docker-compose.prod.yml down`
- Stop development: `docker compose -f docker-compose.development.yml down`
- Stop development & wipe volumes: `docker compose --env-file .env.development -f docker-compose.development.yml down -v`
### List Containers
- docker compose --env-file .env.development -f docker-compose.development.yml ps
### List Containers & Logs
- List containers: `docker compose --env-file .env.development -f docker-compose.development.yml ps`
- Follow logs: `docker compose --env-file .env.development -f docker-compose.development.yml logs -f`
### Logs
- docker compose --env-file .env.development -f docker-compose.development.yml logs -f
---
### Local Development (Host)
1. Start database & cache in Docker:
```bash
docker compose --env-file .env.development -f docker-compose.development.yml up -d postgres redis
```
2. Start API server in watch mode:
```bash
npm run dev
```
---
### Database Migrations & Prisma
- **Generate Prisma Client**:
```bash
npm run prisma:generate
```
- **Run / Apply Dev Migrations**:
```bash
npx dotenv-cli -e .env.development -- npm run prisma:migrate
```
- **Deploy Migrations (Production/CI)**:
```bash
npx dotenv-cli -e .env.development -- npm run prisma:deploy
```
- **Push Schema directly (Sync schema without migration files)**:
```bash
npx dotenv-cli -e .env.development -- npx prisma db push
```
---
### Database Migrations
- **Local (using .env.development):**
- Create/apply new migration: `npx prisma migrate dev --name <name>`
- Push schema directly (prototype/sync): `npx prisma db push`
- Deploy pending migrations: `npm run prisma:deploy`
- **Inside Docker Container:**
- `docker exec -it support-api-development npx prisma migrate deploy`
### Database Seeding
- **Seed Database (Roles, Products, Categories, Hierarchy & Demo data)**:
```bash
npx dotenv-cli -e .env.development -- npm run prisma:seed
```
- **Local:**
- `npm run prisma:seed` (or `npx tsx --env-file=.env.development prisma/seed/index.ts`)
- **Inside Docker Container:**
- `docker exec -it support-api-development npm run prisma:seed`
@@ -10,13 +10,51 @@ export interface CreateEscalationEventData {
triggeredBy: string;
}
export interface CreateEscalationEventResult {
event: EscalationEvent;
/** False when `create` returned a pre-existing event instead of inserting a new one — the
* caller (EscalationService.fire) uses this to skip re-running side effects (reassignment,
* the audit event-bus publish) for a duplicate trigger. */
wasNewlyCreated: boolean;
}
function isDuplicateRuleEscalationConflict(error: unknown): boolean {
return (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002' &&
(error.meta?.target as string[] | undefined)?.includes('ruleId') === true
);
}
export class EscalationEventRepository {
constructor(private readonly prisma = prismaClient) {}
async create(data: CreateEscalationEventData): Promise<EscalationEvent> {
return this.prisma.escalationEvent.create({
data: data as Prisma.EscalationEventUncheckedCreateInput,
});
/**
* 016-load-concurrency-testing research.md §3: a rule-triggered escalation
* (`data.ruleId` set) can only ever legitimately fire once per ticket over that ticket's
* lifetime (SLARun.ticketId is @unique — no reopen-cycle support). The
* `escalation_events_ticket_rule_unique` partial unique index (migration 20260909120000)
* enforces this at the database level; a duplicate trigger (e.g. two overlapping breach-sweep
* passes, or a re-delivered job) fails with `P2002` here, and this method returns the
* pre-existing event instead of throwing — the duplicate is silently absorbed, never
* surfaced as an error to a caller that did nothing wrong. Manual escalations (`ruleId: null`)
* are unaffected and always insert a new row.
*/
async create(data: CreateEscalationEventData): Promise<CreateEscalationEventResult> {
try {
const event = await this.prisma.escalationEvent.create({
data: data as Prisma.EscalationEventUncheckedCreateInput,
});
return { event, wasNewlyCreated: true };
} catch (error) {
if (!isDuplicateRuleEscalationConflict(error)) throw error;
const existing = await this.prisma.escalationEvent.findFirst({
where: { ticketId: data.ticketId, ruleId: data.ruleId },
});
if (!existing) throw error; // conflict raced with a delete — surface the original error.
return { event: existing, wasNewlyCreated: false };
}
}
async findAllForTicket(ticketId: string): Promise<EscalationEvent[]> {
@@ -76,7 +76,7 @@ export class EscalationService {
actor: string,
reason: string,
): Promise<EscalationEvent> {
const event = await this.events.create({
const { event, wasNewlyCreated } = await this.events.create({
ticketId,
ruleId,
// No existing model persists "which hierarchy node is this ticket currently in" — Assignment
@@ -88,6 +88,12 @@ export class EscalationService {
triggeredBy: actor,
});
// 016-load-concurrency-testing research.md §3/FR-003: a duplicate rule-triggered trigger
// (the repository already detected and absorbed it) must not also reassign or re-publish —
// both already happened for the winning attempt; doing them again would be the exact
// duplicate-side-effect bug this feature exists to close.
if (!wasNewlyCreated) return event;
await this.assignments.assignToSpecificNode(ticketId, targetNodeId, actor, reason);
// research.md "SLA_BREACHED/ESCALATION_TRIGGERED are also published, for audit, not for