Happy House - Ecommerce Docs
Developer ResourcesAddress

Address Backend Documentation

Backend architecture, data model, services, cache, and operational behavior for the Address module.

Address - Backend Documentation

1. Documentation Evidence

AreaFiles InspectedVerified Details
Module wiringapps/api/src/modules/address/ module filesLeaf composition, mobile registration
Controllerscustomer/address-customer.controller.tsRoutes, guards, idempotency scopes
Servicescustomer/address-customer.service.tsDefault invariant, advisory lock, serviceability-on-read
DTOscustomer/dto/*.tsPhone/coordinate/ward validation
Schemapackages/db/src/schema/address/customer-address.tsCHECKs, partial unique default, CASCADE
Phoneapps/api/src/utils/phone/phone.util.tsNP region, E.164
Error registryapps/api/src/common/types/error-codes.ts (// CUSTOMER ADDRESS)CUSTOMER_ADDRESS_* codes

2. Backend Scope and Boundaries

Owns

  • The customer address book (seven customer routes; no admin surface — the table holds names, phones and GPS coordinates, and a cross-customer read path should not exist until something concretely needs one).
  • The default-address invariant and the archive/restore lifecycle.
  • Phone normalization for recipient_phone via the shared phone utility.

Does Not Own

  • Order addresses — and this is the single most important sentence in this module's docs: orders must SNAPSHOT the delivery address, never foreign-key to customer_address. fk_customer_address_customer_id is ON DELETE CASCADE (packages/db/src/schema/address/customer-address.ts). When a customer is genuinely erased, every address they own is erased with them — including archived ones. That is correct privacy behaviour (addresses are PII that must not outlive their owner) and the accepted reason this deviates from the usual no-cascade-onto-soft-deletable rule. An order referencing an address row would lose its shipping destination at that moment; an order's address must stay truthful for as long as the order does. Copy the resolved fields — recipient name, phone, province, district, municipality name, ward, street, landmark, postal code, coordinates, delivery instructions — onto the order at checkout time.
  • The same applies to the shipping fee: the order must snapshot the quoted fee. An admin repricing a district must never change what an existing order was charged.
  • Serviceability — computed on read from the live shipping configuration, never stored.

Source of Truth

ConcernSource of TruthNotes
Addressescustomer_address rowsScoped to the customer in every WHERE
DefaultPartial unique index on active rowsSerialized by advisory lock
ServiceabilityLive shipping configurationComputed per read, batched
Order addressesOrder snapshotNever a reference

3. Module Composition

ModuleTypePathControllersProvidersExportsResponsibility
AddressCustomerModuleLeafcustomer/AddressCustomerControllerServiceThe address book
Mobile compositionmobile.module.tsMounted under /api/mobile/addresses

4. File and Directory Map

apps/api/src/modules/address/
  customer/
    address-customer.controller.ts
    address-customer.service.ts
    dto/    create-address.dto.ts  update-address.dto.ts
            list-addresses.query.dto.ts  address-response.dto.ts
packages/db/src/schema/address/
  customer-address.ts
apps/api/src/utils/phone/phone.util.ts      # shared, NP default region

Key files:

FilePurposeKey ExportsNotes
customer/address-customer.service.tsThe book's logicAddressCustomerServiceDefault invariant, lock, serviceability
utils/phone/phone.util.tsPhone normalizationnormalizeToE164NP default; shared with customers.phone
schema/address/customer-address.tsThe tablecustomerAddressCASCADE, partial unique default, CHECKs

5. Data Model

5.1 Schema Source

packages/db/src/schema/address/customer-address.ts

5.2 Tables

customer_address

ColumnTypeNullableIndex/ConstraintRelationNotes
id / public_idserial / uuid v7NoPK / UNIQUE
customer_iduuidNoFK ON DELETE CASCADEcustomers.idPrivacy: addresses never outlive their owner
recipient_namevarcharNo
recipient_phonevarcharNoCHECK — must normalize to E.164Same rule as customers.phone; both must produce byte-identical E.164
district_idintegerNoFK RESTRICTdistrict.idResolved via GeoLookupService
municipality_idintegerYesmunicipality.idAlways null — the reference table ships empty by design
municipality_namevarcharNoFree text the customer types
wardsmallintNoCHECK 1..40Generous; Kathmandu Metropolitan has 32
street / landmark / postal_codevarcharYes
latitude / longitudedouble precisionYesCHECK: both or neither; inside Nepal (26.0–30.6, 79.9–88.3)Pair rule catches transposed coords
delivery_instructionstextYes
is_defaultbooleanNoPartial unique index WHERE is_default AND archived_at IS NULLAt most one default among active rows
archived_attimestamptzYesindexSoft delete; restore clears it

Key CHECKs: chk_customer_address_ward_in_range, coordinate pair/range checks, recipient_phone E.164 normalization check. Active cap (20) is service-enforced; archived rows do not count.

5.3 Relationship Diagram

6. Services and Responsibilities

6.1 AddressCustomerService

MethodCalled ByReadsWritesSide EffectsErrors
create()POSTactive count, districtaddress rowCUSTOMER_ADDRESS_LIMIT_REACHED, CUSTOMER_ADDRESS_DISTRICT_NOT_FOUND, phone/coords codes
list() / findOne()GETown rowsCUSTOMER_ADDRESS_NOT_FOUND (scoped — also the answer for someone else's address; a 403 would be an existence oracle)
update()PATCHactive rowfieldsCUSTOMER_ADDRESS_ALREADY_ARCHIVED, not-found
archive()DELETEactive rowarchived_atdefault promotionCUSTOMER_ADDRESS_ALREADY_ARCHIVED
restore()POST restorearchived rowarchived_at cleareddefault recompute (only if no other active)CUSTOMER_ADDRESS_NOT_ARCHIVED
setDefault()PUT /defaultactive rowsdefault flagsCUSTOMER_ADDRESS_ALREADY_ARCHIVED

Set-default concurrency: a per-customer pg_advisory_xact_lock serializes concurrent calls so two set-defaults cannot race past the partial unique index; the previous default is cleared in the same transaction. restore is idempotency-guarded (customer-address-restore); create too (customer-address-create).

Phone: recipient_phone goes through normalizeToE164 with NP as the default region — local 9812345678 becomes +9779812345678. Both customers.phone and customer_address.recipient_phone must produce byte-identical E.164, or the address CHECK accepts one and rejects the other for the same input.

7. Runtime Flows

7.1 Set default

7.2 Create with serviceability

8. Cache

No address rows are cached — they are per-customer PII and cheap to read. Serviceability rides the shipping cache domain (batched behind one key), so an admin editing a district refreshes every address list on the next read.

9. Jobs and Workers

None. The address book is synchronous.

10. Security and Authorization

  • Every route is scoped to the authenticated customer inside the service's WHERE clauses — there is no admin surface over this table, deliberately.
  • CUSTOMER_READ 60/min and CUSTOMER_WRITE 20/min, both keyed on the account (keyStrategy: "user"), so customers behind one carrier NAT do not exhaust each other's quota.
  • Cross-customer reads return 404 CUSTOMER_ADDRESS_NOT_FOUND — never 403, which would be an existence oracle.
  • Create and restore carry Idempotency-Key requirements (scopes customer-address-create, customer-address-restore).

11. Operational Notes

  • The municipality reference ships empty — do not treat municipality.id: null as a bug; it is the designed state, and municipality.name is the truth.
  • Restoring an archived address never restores its old default flag — the flag is recomputed, and the address becomes default only when the customer has no other active addresses.
  • A customer with zero active addresses has no default — legal, not an error.