Skip to main content

Carts & Items Inventory

The inventory system manages golf cart fleet and items/add-ons at each course, with reservation tracking and optional Facilities integration.

Architecture

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│ Admin UI │────▶│ CourseInventory │────▶│ Cart/Item │
│ (inventory) │ │ Controller │ │ Repositories │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌─────────────────┐
│ Facilities │ │ PostgreSQL │
│ Cart Adapter │ │ (inventory) │
└──────────────────┘ └─────────────────┘

Sources:

  • libs/tee-time-services/src/lib/facade/course-inventory.controller.ts
  • libs/prisma/teetime-client/src/repositories/course-cart-inventory.repository.ts
  • libs/prisma/teetime-client/src/repositories/course-item-inventory.repository.ts

Cart Inventory

Cart Types

TypeDescription
ELECTRICElectric golf cart
PULLPull/push cart (manual)
PUSHPush cart

Cart Status

StatusDescription
ACTIVEAvailable for reservation
INACTIVENot currently in use
MAINTENANCEUnder repair/service

Cart Model

interface CourseCartInventory {
id: string;
courseId: string;
type: 'ELECTRIC' | 'PULL' | 'PUSH';
totalUnits: number;
availableUnits: number;
reservedUnits: number;
pricePerRound: number; // Decimal (stored as cents in API)
status: 'ACTIVE' | 'INACTIVE' | 'MAINTENANCE';
createdAt: Date;
updatedAt: Date;
}

Cart API

// List carts
GET /v1/courses/:courseId/carts
// Query params: type, status

// Create cart
POST /v1/courses/:courseId/carts
{
"type": "electric",
"totalUnits": 20,
"availableUnits": 20,
"pricePerRoundCents": 3500 // $35.00
}

// Update cart
PUT /v1/courses/:courseId/carts/:id
{
"availableUnits": 18,
"status": "active"
}

// Delete cart
DELETE /v1/courses/:courseId/carts/:id

Item Inventory

Item Categories

CategoryDescriptionExamples
RENTALEquipment rentalsClubs, shoes, rangefinder
SALEItems for purchaseBalls, gloves, tees
SNACKFood & beverageWater, snacks

Item Status

Status is automatically calculated based on stock:

StatusCondition
IN_STOCKstockLevel > lowStockThreshold
LOW_STOCKstockLevel <= lowStockThreshold && stockLevel > 0
OUT_OF_STOCKstockLevel === 0

Item Model

interface CourseItemInventory {
id: string;
courseId: string;
name: string;
description?: string;
category: 'RENTAL' | 'SALE' | 'SNACK';
stockLevel: number;
lowStockThreshold: number; // Default: 5
price: number; // Decimal
status: 'IN_STOCK' | 'LOW_STOCK' | 'OUT_OF_STOCK'; // Computed
createdAt: Date;
updatedAt: Date;
}

Item API

// List items
GET /v1/courses/:courseId/items
// Query params: category, status, lowStockOnly

// Create item
POST /v1/courses/:courseId/items
{
"name": "Premium Golf Balls (Dozen)",
"category": "sale",
"stockLevel": 50,
"lowStockThreshold": 10,
"priceCents": 4500 // $45.00
}

// Update item
PUT /v1/courses/:courseId/items/:id
{
"stockLevel": 45
}

// Get low stock count
// (returns count in response metadata)
GET /v1/courses/:courseId/items?lowStockOnly=true

Cart Reservations

Track cart assignments to bookings:

Reservation Status

StatusDescription
PENDINGReserved, not yet picked up
CONFIRMEDCart in use
RETURNEDCart returned
CANCELLEDReservation cancelled

Reservation Model

interface CourseCartReservation {
id: string;
cartId: string;
bookingId?: string;
playerId?: string;
teeTimeId?: string;
reservationDate: Date;
status: 'PENDING' | 'CONFIRMED' | 'RETURNED' | 'CANCELLED';
createdAt: Date;
updatedAt: Date;

// Relations
cart?: CourseCartInventory;
player?: Player;
}

Reservation API

// List reservations for date
GET /v1/courses/:courseId/cart-reservations?date=2025-12-15

// Response
[
{
"id": "res-123",
"cartId": "cart-456",
"bookingId": "booking-789",
"playerId": "player-abc",
"reservationDate": "2025-12-15",
"status": "confirmed",
"cart": {
"type": "electric",
"pricePerRoundCents": 3500
},
"player": {
"firstName": "John",
"lastName": "Smith"
}
}
]

Additional Item Reservations

Track add-ons purchased with bookings:

interface CourseAdditionalItemReservation {
id: string;
itemId: string;
bookingId: string;
quantity: number;
totalPrice: number;
status: 'PENDING' | 'CONFIRMED' | 'CANCELLED';
createdAt: Date;

// Relations
item?: CourseItemInventory;
booking?: Booking;
}

Facilities Integration

For courses using centralized Facilities cart management:

Configuration

// Course model
{
"useFacilitiesCarts": true,
"facilitiesClubId": "facilities-club-123",
"facilitiesTenantId": "facilities-tenant-456"
}

Adapter Behavior

When useFacilitiesCarts is enabled:

OperationBehavior
GET /cartsDelegates to Facilities API
POST /cartsBlocked (read-only)
PUT /cartsBlocked (read-only)
DELETE /cartsBlocked (read-only)
GET /reservationsMerges Facilities + local data
// In FacilitiesCartAdapter
async listCarts(courseId: string): Promise<CartInventory[]> {
if (course.useFacilitiesCarts) {
return this.facilitiesClient.getCarts(course.facilitiesClubId);
}
return this.cartRepository.findAll({ courseId });
}

Course Status Integration

When course status transitions to CLOSED:

  1. All cart inventory marked INACTIVE
  2. Pending reservations marked RETURNED
  3. No new reservations allowed

When status returns to OPEN:

  1. Cart inventory restored to ACTIVE
  2. New reservations enabled

See Course Status for details.

Inventory Summary

Get aggregate inventory stats:

// GET /v1/courses/:courseId/carts/summary
{
"totalCarts": 30,
"availableCarts": 25,
"reservedCarts": 5,
"byType": {
"electric": { "total": 20, "available": 16 },
"pull": { "total": 10, "available": 9 }
}
}

// GET /v1/courses/:courseId/items/summary
{
"totalItems": 15,
"inStock": 12,
"lowStock": 2,
"outOfStock": 1
}

Currency Handling

API uses cents for all prices; database stores decimals:

// API → Database
const priceDecimal = dto.priceCents / 100;

// Database → API
const priceCents = Math.round(entity.price * 100);

Data Model (Prisma)

model CourseCartInventory {
id String @id @default(uuid())
courseId String
type CartType
totalUnits Int
availableUnits Int
reservedUnits Int @default(0)
pricePerRound Decimal @db.Decimal(10, 2)
status CartStatus @default(ACTIVE)

course Course @relation(fields: [courseId])
reservations CourseCartReservation[]

@@index([courseId, type])
}

model CourseItemInventory {
id String @id @default(uuid())
courseId String
name String
description String?
category ItemCategory
stockLevel Int
lowStockThreshold Int @default(5)
price Decimal @db.Decimal(10, 2)
status ItemStatus @default(IN_STOCK)

course Course @relation(fields: [courseId])
reservations CourseAdditionalItemReservation[]

@@index([courseId, category])
}

enum CartType {
ELECTRIC
PULL
PUSH
}

enum CartStatus {
ACTIVE
INACTIVE
MAINTENANCE
}

enum ItemCategory {
RENTAL
SALE
SNACK
}

enum ItemStatus {
IN_STOCK
LOW_STOCK
OUT_OF_STOCK
}

Metrics

MetricDescription
cart_inventory_totalCarts by type and status
cart_utilization_rateReserved / available ratio
item_inventory_low_stockItems below threshold
cart_reservation_totalReservations by status

Troubleshooting

Cart Availability Mismatch

  1. Verify reservation status updates
  2. Check for orphaned reservations
  3. Reconcile availableUnits with actual reservations

Facilities Integration Issues

  1. Verify useFacilitiesCarts flag
  2. Check Facilities API connectivity
  3. Review tenant/club ID configuration

Low Stock Alerts Not Triggering

  1. Verify lowStockThreshold is set
  2. Check stock level updates
  3. Review notification service connectivity