From 805a67d992e9b94dba5e69090b8a6eb078bf2337 Mon Sep 17 00:00:00 2001 From: MastersGame Date: Sat, 7 Nov 2026 13:12:35 +0100 Subject: [PATCH] chore: add Claude Code configuration - add CLAUDE.md (project description, architecture, domain notes, git workflow) - add shared .claude/settings.json (allowed commands, .env read protection) - pin Prisma agent skills in skills-lock.json and document how to restore them - ignore generated skills, local settings and Postman skills in .gitignore - add .gitattributes to normalize line endings to LF Co-Authored-By: Claude Sonnet 5.5 --- .claude/settings.json | 17 ++++++++++ .gitattributes | 1 + .gitignore | 12 ++++++++ CLAUDE.md | 72 +++++++++++++++++++++++++++++++++++++++++++ README.md | 8 +++++ skills-lock.json | 59 +++++++++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+) create mode 100644 .claude/settings.json create mode 100644 .gitattributes create mode 100644 CLAUDE.md create mode 100644 skills-lock.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..6e2196f --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,17 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run lint)", + "Bash(npm run build)", + "Bash(npm test)", + "Bash(npm run test:e2e)", + "Bash(npx jest:*)", + "Bash(npx prisma generate --config prisma7.config.ts)" + ], + "deny": [ + "Read(./.env)", + "Read(./.env.*)", + "Bash(cat .env*)" + ] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.gitignore b/.gitignore index 7e0eb9f..f388715 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,15 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Prisma client (regenerated by `prisma generate`, run on npm install) /prisma/generated/ + +# AI agent skills installed from skills-lock.json (keep skills-lock.json committed) +/.agents/ +/.windsurf/ +/.claude/skills/ +# Claude Code personal settings (.claude/settings.json stays shareable) +/.claude/settings.local.json + +# Postman (skills fetched by `postman init`, personal workspace binding) +/postman/ +/.postman/ +/AGENTS.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e70def7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,72 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +Backend of a K-pop online shop. The real shop (catalog, stock, customers, sales) lives in **Hiboutik**, a POS/e-commerce platform; Hiboutik stays the source of truth. This backend sits in front of it as a buffer: it mirrors Hiboutik data into its own database so the storefront does not hammer the Hiboutik API with requests. It is also the place to build shop features Hiboutik does not offer, on top of the mirrored data. + +Stack: NestJS 11, PostgreSQL via **Prisma 7**, Hiboutik REST API as the upstream. + +## Commands + +```bash +npm run start:dev # watch mode (port from PORT, default 3000) +npm run build # nest build -> dist/ +npm run lint # eslint --fix on src/test +npm run format # prettier (singleQuote, trailingComma all, printWidth 140) +npm test # unit tests (*.spec.ts under src/) +npx jest src/app.controller.spec.ts # single test file +npx jest -t "test name" # single test by name +npm run test:e2e # e2e tests (test/jest-e2e.json) +``` + +Jest runs in CommonJS: its config (in `package.json` and `test/jest-e2e.json`) transpiles the ESM-only `@nestjs/config` (`transformIgnorePatterns`) and maps `./x.js` imports to `./x` (`moduleNameMapper`) for the generated Prisma client. Keep both configs in sync. Unit tests build services directly with mocked repositories; the e2e test boots `AppModule` with `PrismaService` and `HiboutikService` overridden, so it needs neither a database nor Hiboutik. + +Prisma: the config file is `prisma7.config.ts` (non-default name), so pass it explicitly: + +```bash +npx prisma generate --config prisma7.config.ts +npx prisma migrate dev --name --config prisma7.config.ts +``` + +The client is generated into `prisma/generated/` (generator `prisma-client`, not the legacy `prisma-client-js`) and must be regenerated after any `schema.prisma` change. Code imports types from there, e.g. `prisma/generated/client` and `prisma/generated/models/` (`ProductCreateInput`, etc.). + +## Environment + +Read from `.env` (`@nestjs/config`, global): `DATABASE_URL` (used by Prisma and `PrismaService`), `HIBOUTIK_BASE_URL`, `HIBOUTIK_USERNAME`, `HIBOUTIK_PASSWORD` (HTTP Basic auth). HTTP calls to Hiboutik time out after 30 s (`HttpModule.register` in `AppModule`). + +## Architecture + +Everything is registered as providers in a single `AppModule` (`src/app.module.ts`); new repositories/converters/services must be added there. `PrismaModule` (in `prisma/`, outside `src/`) is global. + +Sync pipeline, one entity per layer: + +1. **`src/hiboutik/*.hiboutik.repository.ts`** – fetch raw data from Hiboutik through `HiboutikClient` (`get`, and `getAllPages`, which walks the `p` page param until a page smaller than 250 results, Hiboutik's page size). `/suppliers` and `/stock_available/all_wh` are not paginated. +2. **`src/models/hiboutikDTO/`** – snake_case types mirroring Hiboutik API payloads. +3. **`src/models/converter/`** – classes extending abstract `Converter`; `fromHiboutik` maps a DTO to a Prisma `*CreateInput`. Relations use `connect: { hiboutikId }`. +4. **`src/database/*.database.repository.ts`** – Prisma access, mainly `upsert` keyed on the unique `hiboutikId`. +5. **`src/services/hiboutik/*.hiboutik.service.ts`** – per-entity sync orchestration; `HiboutikService.syncAll` runs them in dependency order: brands → categories → suppliers → products → customers (products `connect` to the first three by `hiboutikId`, so order matters). + +Domain notes: +- Synced models carry a unique `hiboutikId` (join key with Hiboutik), except `Order` (nullable `hiboutikSaleId`), `ProductVersion` (unique `[productId, hiboutikSizeId]`) and `OrderLine`; the internal `id` is a UUID. Empty Hiboutik barcodes are stored as `null`. `ProductVersion.barcode` is indexed but **not unique** (Hiboutik may reuse a barcode). +- Hiboutik is the source of truth: brands, categories, suppliers, products and customers missing from it are deleted (skipped if Hiboutik returns an empty list). Products are deleted before the upserts (so a recreated product can reuse its barcode); their versions go by cascade, and versions missing from a product are deleted before its versions are upserted. Customer addresses go by cascade with their customer; other relations (product brand/category/supplier, sub-categories, order customer) are set to null and orders are kept. +- Brands, categories and suppliers are upserted through `forEachSafely` (`src/utils/sync.ts`): a failing row is logged and skipped. Each sync step and each product/customer is in a try/catch: failures are logged, the sync goes on, and a summary is logged at the end. +- Categories form a tree: parents are linked in a second pass (`setParent`). `enabled` mirrors `category_enabled` (POS), `webEnabled` mirrors `category_enabled_www`. +- Products: `ProductVersion` rows come from `product_size_details` (barcode, price/discount overrides from `product_specific_rules`, stock from the stocks endpoint); no sizes means one version with `hiboutikSizeId: 0`. `globalStock` is the sum of available stock. `archived` mirrors `product_arch`. +- `GET /customer/:id` embeds `addresses` and `sales` (typed in `customer.hiboutik.dto.ts`); `sales` only has headers, lines come from `GET /sales/:id` (`SaleHiboutikRepository`). +- Addresses (from the embedded array): missing ones are deleted, the rest upserted via `forEachSafely`; no array means stored addresses are left untouched. Each has its own recipient fields, empty optional values stored as `null`. Company details (`taxNumber`, `companyNumber`, `legalStatus`) come from `GET /customers_addresses/:id`, only for addresses with a `company` (`null` otherwise, untouched if the call fails). +- Orders come from the customer's `sales`: sales already stored as `COMPLETED` are skipped, the others are fetched and upserted on `hiboutikSaleId` (`source: STORE` on creation only). `status` is `COMPLETED` or `PENDING`, `orderedAt` is the completion date else the creation date. Lines are replaced on each upsert and snapshot name, version, barcode, price and Hiboutik ids; the `ProductVersion` link is optional (`SetNull`). `unitPrice` is the paid price, taxes and discount included. Quotations are skipped, anonymous sales are never synced (only customers' sales are read), and orders are never deleted. +- Shared Prisma payload types (e.g. `ProductWithVersions`) live in `prisma/customObjects/`. + +## API + +If the Postman plugin is installed, load its `postman:api-engineer` skill before any non-trivial API engineering task (designing, implementing, mocking, testing, monitoring, documenting, deploying). Pure questions and trivial one-line edits do not need it. The local `postman/` and `.postman/` folders are generated by `postman init` and are git-ignored. + +## Git workflow + +The remote is a Gitea instance (`origin`). Use plain `git` or the Gitea CLI (`gt`) for pull requests and other Gitea operations. + +- Never commit or push directly to the integration branch: always push changes on a `feature/` branch. +- Merges go into `develop`. Until `develop` exists (early project phase), they go into `main`. +- Commit messages follow Conventional Commits (`feat(sync): ...`, `fix(db): ...`). diff --git a/README.md b/README.md index 8f0f65f..0d11a51 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,14 @@ $ npm install ``` +### AI agent skills (optional) + +The Prisma skills used by Claude Code are pinned in `skills-lock.json`; the skill folders themselves are not committed. Restore them with: + +```bash +$ npx skills experimental_install +``` + ## Compile and run the project ```bash diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..aacff56 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,59 @@ +{ + "version": 1, + "skills": { + "prisma-cli": { + "source": "prisma/skills", + "sourceType": "github", + "skillPath": "prisma-cli/SKILL.md", + "computedHash": "b92e55aef78f6796d81433c44738a6733211a04c16e65cbad6d42c5f71092aab" + }, + "prisma-client-api": { + "source": "prisma/skills", + "sourceType": "github", + "skillPath": "prisma-client-api/SKILL.md", + "computedHash": "5dcc0793337151efa73a444e5adbc7e1c24180778e83b4fe94c9109c391bd333" + }, + "prisma-compute": { + "source": "prisma/skills", + "sourceType": "github", + "skillPath": "prisma-compute/SKILL.md", + "computedHash": "e5695a3c6f35428e96df30ae662716bd4c9d9074f72cc9334b3050445a1bd4e2" + }, + "prisma-database-setup": { + "source": "prisma/skills", + "sourceType": "github", + "skillPath": "prisma-database-setup/SKILL.md", + "computedHash": "0911d9454bd48df3badd23a50fd90a280eabb15dcbd472c14e7b729075dadefb" + }, + "prisma-driver-adapter-implementation": { + "source": "prisma/skills", + "sourceType": "github", + "skillPath": "prisma-driver-adapter-implementation/SKILL.md", + "computedHash": "07484627aea6cce4d0f94b4090ff9acc0caa7e104f9988b95abecf80132f4103" + }, + "prisma-mongodb-upgrade": { + "source": "prisma/skills", + "sourceType": "github", + "skillPath": "prisma-mongodb-upgrade/SKILL.md", + "computedHash": "f9ba440e88ca4cec9801d04762296e29e99ca08cb8a8156e4f783ecf90279c8f" + }, + "prisma-postgres": { + "source": "prisma/skills", + "sourceType": "github", + "skillPath": "prisma-postgres/SKILL.md", + "computedHash": "d669fbd0d8017d16c967f18b20e1c1345cd4a2a0076d762459bfef79f551f655" + }, + "prisma-postgres-setup": { + "source": "prisma/skills", + "sourceType": "github", + "skillPath": "prisma-postgres-setup/SKILL.md", + "computedHash": "c89d3aa91285d8e4964fcaa5238c99a3d3c20b617e032e731cf632330a85a5a6" + }, + "prisma-upgrade-v7": { + "source": "prisma/skills", + "sourceType": "github", + "skillPath": "prisma-upgrade-v7/SKILL.md", + "computedHash": "dcc6c71adca6b22f37c5bda5ac7fd63c3bb59495596a22e06a29c7c85371558f" + } + } +}