feat(db): add Prisma schema and global PrismaModule

Models mirror Hiboutik entities (categories tree, brands, suppliers, products and their versions, customers) plus addresses and orders for the upcoming sync. Every synced model is matched with Hiboutik through a unique hiboutikId. The generated client is ignored by git and env files other than .env.example are ignored.
This commit is contained in:
MastersGame
2026-11-05 23:00:46 +01:00
parent a0ddef240c
commit 8f377a1fe9
7 changed files with 271 additions and 4 deletions

View File

@@ -0,0 +1,4 @@
import { Prisma } from '../generated/client';
/** Product loaded with its versions (`include: { versions: true }`). */
export type ProductWithVersions = Prisma.ProductGetPayload<{ include: { versions: true } }>;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

9
prisma/prisma.module.ts Normal file
View File

@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

20
prisma/prisma.service.ts Normal file
View File

@@ -0,0 +1,20 @@
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from './generated/client';
import { PrismaPg } from '@prisma/adapter-pg';
/** Prisma client shared by the whole app (global PrismaModule), connected to `DATABASE_URL` through the pg driver adapter. */
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
constructor() {
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
super({ adapter });
}
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}

216
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,216 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Get a free hosted Postgres database in seconds: `npx create-db`
generator client {
provider = "prisma-client"
output = "./generated/"
}
datasource db {
provider = "postgresql"
}
/// Product category, mirrored from Hiboutik. Categories form a tree (parent / children).
model Category {
id String @id @default(uuid())
updatedAt DateTime @updatedAt
/// Enabled in the Hiboutik POS (category_enabled)
enabled Boolean @default(true)
hiboutikId Int @unique
name String
/// Display order among its siblings (category_position)
position Int @default(0)
/// Displayed on the website (category_enabled_www)
webEnabled Boolean @default(true)
/// Null for a root category
parent Category? @relation("CategoryTree", fields: [parentId], references: [id], onDelete: SetNull)
parentId String?
children Category[] @relation("CategoryTree")
products Product[]
@@map("categories")
}
/// Product brand, mirrored from Hiboutik.
model Brand {
id String @id @default(uuid())
updatedAt DateTime @updatedAt
enabled Boolean @default(true)
hiboutikId Int @unique
name String
products Product[]
@@map("brands")
}
/// Product supplier, mirrored from Hiboutik.
model Supplier {
id String @id @default(uuid())
updatedAt DateTime @updatedAt
enabled Boolean @default(true)
hiboutikId Int @unique
name String
products Product[]
@@map("suppliers")
}
/// Product mirrored from Hiboutik. What is actually sold is one of its versions (sizes).
model Product {
id String @id @default(uuid())
updatedAt DateTime @updatedAt
/// Archived in Hiboutik (product_arch)
archived Boolean @default(false)
barcode String?
description String
/// Null when the product has no discount
discountPrice Decimal? @db.Decimal(10, 2)
/// Displayed on the website (product_display_www)
enabled Boolean @default(true)
/// Sum of the available stock of all its versions
globalStock Int @default(0)
hiboutikId Int @unique
/// Last update in Hiboutik
hiboutikUpdatedAt DateTime?
name String
price Decimal @db.Decimal(10, 2)
brand Brand? @relation(fields: [brandId], references: [id], onDelete: SetNull)
brandId String?
category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
categoryId String?
supplier Supplier? @relation(fields: [supplierId], references: [id], onDelete: SetNull)
supplierId String?
versions ProductVersion[]
@@map("products")
}
/// Sellable version of a product: one per Hiboutik size, or a single one (hiboutikSizeId 0) for a product without sizes.
model ProductVersion {
id String @id @default(uuid())
updatedAt DateTime @updatedAt
/// Not unique: Hiboutik is the source of truth and may hold the same barcode on several products
barcode String?
/// Size-specific discount, null when none
discountPrice Decimal? @db.Decimal(10, 2)
/// Hiboutik size id, 0 for a product without sizes
hiboutikSizeId Int
/// Size-specific price, null to use the product price (copy of the product price for a product without sizes)
price Decimal? @db.Decimal(10, 2)
stock Int @default(0)
/// Size name, or the product name for a product without sizes
versionName String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade, onUpdate: Restrict)
productId String
orderLines OrderLine[]
@@unique([productId, hiboutikSizeId])
@@index([barcode])
@@map("productVersions")
}
/// Customer mirrored from Hiboutik.
model Customer {
id String @id @default(uuid())
updatedAt DateTime @updatedAt
dateOfBirth DateTime?
/// Kept when removed in Hiboutik (an empty Hiboutik email does not erase it)
email String?
firstName String
hiboutikId Int @unique
/// Last update in Hiboutik
hiboutikUpdatedAt DateTime?
lastName String
phone String?
addresses CustomerAddress[]
orders Order[]
@@index([email])
@@map("customers")
}
/// Address of a customer, mirrored from Hiboutik (sync not implemented yet).
model CustomerAddress {
id String @id @default(uuid())
updatedAt DateTime @updatedAt
address String
city String
country String
hiboutikId Int @unique
isDefault Boolean @default(false)
zipCode String
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
customerId String
@@map("customer_addresses")
}
/// Where an order was placed.
enum OrderSource {
STORE // hiboutik
WEB // site
}
/// Order placed in store (Hiboutik sale) or on the website.
model Order {
id String @id @default(uuid())
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
/// Null for a website order not (yet) sent to Hiboutik
hiboutikSaleId Int? @unique
orderedAt DateTime
source OrderSource
status String
total Decimal @db.Decimal(10, 2)
/// Null for an anonymous sale or a deleted customer
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
customerId String?
lines OrderLine[]
@@map("orders")
}
/// Line of an order.
model OrderLine {
id String @id @default(uuid())
updatedAt DateTime @updatedAt
// Snapshot of the product at order time: the catalog can change or be deleted afterwards
barcode String?
hiboutikProductId Int
hiboutikSizeId Int
productName String
quantity Int
unitPrice Decimal @db.Decimal(10, 2)
versionName String
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
orderId String
// Link to the current catalog, set to null when the version is deleted
productVersion ProductVersion? @relation(fields: [productVersionId], references: [id], onDelete: SetNull)
productVersionId String?
@@map("order_lines")
}