08 — Form Specifications (Library Module)
Field-level contracts for the three forms. Server validation is authoritative; client validation mirrors it for latency only. DTO sources:
create-book.dto.ts,update-book.dto.ts,issue-book.dto.ts,return-book.dto.ts.
F1. Book form (create / edit)
Create body = CreateBookDto (create-book.dto.ts:4-47); edit body = UpdateBookDto
= PartialType(CreateBookDto) (update-book.dto.ts:4) — every field optional on edit.
| Field | Type | Required | Constraints (server) | Notes |
|---|---|---|---|---|
title | string | ✓ | @IsString (:6-7) | trimmed server-side (book.schema.ts:16-17) |
author | string | ✓ | @IsString (:10-11) | trimmed (book.schema.ts:19-20) |
isbn | string | ✓ | @IsString (:14-15) | trimmed; unique per tenant — 409 on duplicate (library.service.ts:35-40, book.schema.ts:51) |
publisher | string | – | @IsOptional @IsString (:17-20) | |
category | string | – | @IsOptional @IsString (:22-25) | client filter chip source (OQ-5) |
edition | string | – | @IsOptional @IsString (:27-30) | |
totalCopies | number | – | @IsOptional @IsNumber @Min(1) (:32-36) | default 1 (library.service.ts:41); create sets availableCopies = totalCopies (:44-46); edit shifts availableCopies by Δ, floor 0 (library.service.ts:87-90) |
shelfLocation | string | – | @IsOptional @IsString (:38-41) | |
description | string | – | @IsOptional @IsString (:43-46) |
Validation order on submit: required → type → ISBN uniqueness hint (client may pre-check from cache, but the server 409 is authoritative).
Copy projection widget: create → "Available will equal total (N)"; edit → "Available: 2 → 3" using the Δ rule.
F2. Issue form
Body = IssueBookDto (issue-book.dto.ts:4-21).
| Field | Type | Required | Constraints (server) | Notes |
|---|---|---|---|---|
bookId | string | ✓ | @IsString (:6-7) | from Book Detail context; 404 if unknown (library.service.ts:108) |
studentId | string | ✓ | @IsString (:9-10) | from StudentPicker; ref Student (borrow-record.schema.ts:25-26) |
dueDate | string (ISO) | ✓ | @IsDateString (:14-15) | no server min-date rule — client must require future dates; new Date(dueDate) stored (library.service.ts:135) |
notes | string | – | @IsOptional @IsString (:17-20) | stored on record (library.service.ts:138) |
Server-side gates (client shows the same copy pre-submit where possible):
| Gate | Condition | Error |
|---|---|---|
| Availability | book.availableCopies < 1 | 409 "No copies available for borrowing." (library.service.ts:109-111) |
| Student cap | active borrows ≥ 5 | 409 "Student already has maximum number of borrowed books." (library.service.ts:116-120) |
Defaults: due date = today + 14 days (client default only; no server default —
dueDate required).
On success: record created with status: ACTIVE, borrowedAt: now
(library.service.ts:132-139); book availableCopies − 1, status → borrowed when
0 remain (library.service.ts:122-130).
F3. Return form
Body = ReturnBookDto (return-book.dto.ts:4-19).
| Field | Type | Required | Constraints (server) | Notes |
|---|---|---|---|---|
borrowRecordId | string | ✓ | @IsString (:6-7) | 404 if unknown (library.service.ts:153-154); 409 if not ACTIVE (library.service.ts:155-157) |
fineAmount | number | – | @IsOptional @IsNumber @Min(0) (:9-13) | staff override: server prefers dto.fineAmount ?? calculateFine(dueDate) (library.service.ts:159); 0 is legal (on-time or waived-by-zero) |
notes | string | – | @IsOptional @IsString (:15-18) | replaces record notes on return (library.service.ts:167) |
Fine computation (server, authoritative):
calculateFine(dueDate): 0 if now <= dueDate; else ceil((now - dueDate) / 1 day) × 5
(library.service.ts:211-217; DAILY_FINE_RATE = 5 at :25)
On success: status → RETURNED, returnedAt: now, fineAmount set,
fineStatus → pending iff fineAmount > 0 (else left unset) (library.service.ts:161-169);
book availableCopies + 1, status → available (library.service.ts:172-178).
Post-return follow-up: if fineAmount > 0, offer "Pay fine" →
POST /books/fines/:borrowRecordId/pay (no body — library.controller.ts:85-89)
→ fineStatus: PAID (library.service.ts:203-209). Server does not guard pay state
(OQ-6): client only offers it on fineStatus == pending.
F4. Validation & error UX (all forms)
| Code | Meaning | UI |
|---|---|---|
400 VALIDATION_ERROR | details[].message per field | inline field errors, focus first invalid |
409 DUPLICATE_RESOURCE | ISBN dup / no copies / cap / not-active | banner + contextual action (edit ISBN / refresh availability) |
404 RESOURCE_NOT_FOUND | book/record gone | close form, snackbar |
429 RATE_LIMITED | api tier | countdown, no auto-retry |
| 5xx | server | generic + requestId, manual retry |
Envelope + codes: response-envelope.interceptor.ts:44-52, http-exception.filter.ts:28-34,74-78.
No optimistic writes in any form — every submit awaits the server response
(00-shared/07 §9).