From cb8e6a0ec33563494101b3adef91b3e883d0ee10 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Tue, 5 May 2026 21:25:01 +0200 Subject: [PATCH] fix(loyalty): accept store_id in body for merchant PIN create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merchant /pins POST was reading store_id as a query parameter, but the shared loyalty pins JS factory sends the form (including store_id) as a JSON body — matching the store-side endpoint, which gets store_id from the JWT and ignores any body field. Result: a 422 "Field required" on every PIN create from /merchants/loyalty/pins. Add PinCreateForMerchant (PinCreate + store_id) and switch the endpoint to it. Validation that the store belongs to the merchant is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/modules/loyalty/routes/api/merchant.py | 15 +++++++++------ app/modules/loyalty/schemas/__init__.py | 2 ++ app/modules/loyalty/schemas/pin.py | 8 ++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/modules/loyalty/routes/api/merchant.py b/app/modules/loyalty/routes/api/merchant.py index 87bdddb0..58d2a604 100644 --- a/app/modules/loyalty/routes/api/merchant.py +++ b/app/modules/loyalty/routes/api/merchant.py @@ -38,6 +38,7 @@ from app.modules.loyalty.schemas import ( CardResponse, MerchantSettingsResponse, PinCreate, + PinCreateForMerchant, PinDetailListResponse, PinDetailResponse, PinResponse, @@ -339,22 +340,24 @@ def list_pins( @router.post("/pins", response_model=PinResponse, status_code=201) def create_pin( - data: PinCreate, - store_id: int = Query(..., gt=0), + data: PinCreateForMerchant, merchant: Merchant = Depends(get_merchant_for_current_user), db: Session = Depends(get_db), ): - """Create a new staff PIN.""" + """Create a new staff PIN. ``store_id`` comes from the body — the + merchant portal isn't scoped to a single store like the store API is. + """ # Validate store belongs to merchant locations = program_service.get_merchant_locations(db, merchant.id) store_ids = [loc.id for loc in locations] - if store_id not in store_ids: + if data.store_id not in store_ids: from app.modules.tenancy.exceptions import StoreNotFoundException - raise StoreNotFoundException(str(store_id), identifier_type="id") + raise StoreNotFoundException(str(data.store_id), identifier_type="id") program = program_service.require_program_by_merchant(db, merchant.id) - pin = pin_service.create_pin(db, program.id, store_id, data) + pin_data = PinCreate(name=data.name, staff_id=data.staff_id, pin=data.pin) + pin = pin_service.create_pin(db, program.id, data.store_id, pin_data) return PinResponse.model_validate(pin) diff --git a/app/modules/loyalty/schemas/__init__.py b/app/modules/loyalty/schemas/__init__.py index 4e6da940..e073df4a 100644 --- a/app/modules/loyalty/schemas/__init__.py +++ b/app/modules/loyalty/schemas/__init__.py @@ -39,6 +39,7 @@ from app.modules.loyalty.schemas.card import ( from app.modules.loyalty.schemas.pin import ( # Staff PIN PinCreate, + PinCreateForMerchant, PinDetailListResponse, PinDetailResponse, PinListResponse, @@ -131,6 +132,7 @@ __all__ = [ "PointsAdjustResponse", # PIN "PinCreate", + "PinCreateForMerchant", "PinUpdate", "PinResponse", "PinDetailResponse", diff --git a/app/modules/loyalty/schemas/pin.py b/app/modules/loyalty/schemas/pin.py index 87a8b2e7..c65b6e2f 100644 --- a/app/modules/loyalty/schemas/pin.py +++ b/app/modules/loyalty/schemas/pin.py @@ -31,6 +31,14 @@ class PinCreate(BaseModel): ) +class PinCreateForMerchant(PinCreate): + """PinCreate from the merchant portal — carries the target store_id in + the body since the merchant has no per-store auth context (unlike the + store-side endpoint which reads store_id from the JWT).""" + + store_id: int = Field(..., gt=0, description="Store this PIN belongs to") + + class PinUpdate(BaseModel): """Schema for updating a staff PIN."""