fix(loyalty): accept store_id in body for merchant PIN create
Some checks failed
CI / ruff (push) Successful in 15s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 21:25:01 +02:00
parent 573b0ef483
commit cb8e6a0ec3
3 changed files with 19 additions and 6 deletions

View File

@@ -38,6 +38,7 @@ from app.modules.loyalty.schemas import (
CardResponse, CardResponse,
MerchantSettingsResponse, MerchantSettingsResponse,
PinCreate, PinCreate,
PinCreateForMerchant,
PinDetailListResponse, PinDetailListResponse,
PinDetailResponse, PinDetailResponse,
PinResponse, PinResponse,
@@ -339,22 +340,24 @@ def list_pins(
@router.post("/pins", response_model=PinResponse, status_code=201) @router.post("/pins", response_model=PinResponse, status_code=201)
def create_pin( def create_pin(
data: PinCreate, data: PinCreateForMerchant,
store_id: int = Query(..., gt=0),
merchant: Merchant = Depends(get_merchant_for_current_user), merchant: Merchant = Depends(get_merchant_for_current_user),
db: Session = Depends(get_db), 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 # Validate store belongs to merchant
locations = program_service.get_merchant_locations(db, merchant.id) locations = program_service.get_merchant_locations(db, merchant.id)
store_ids = [loc.id for loc in locations] 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 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) 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) return PinResponse.model_validate(pin)

View File

@@ -39,6 +39,7 @@ from app.modules.loyalty.schemas.card import (
from app.modules.loyalty.schemas.pin import ( from app.modules.loyalty.schemas.pin import (
# Staff PIN # Staff PIN
PinCreate, PinCreate,
PinCreateForMerchant,
PinDetailListResponse, PinDetailListResponse,
PinDetailResponse, PinDetailResponse,
PinListResponse, PinListResponse,
@@ -131,6 +132,7 @@ __all__ = [
"PointsAdjustResponse", "PointsAdjustResponse",
# PIN # PIN
"PinCreate", "PinCreate",
"PinCreateForMerchant",
"PinUpdate", "PinUpdate",
"PinResponse", "PinResponse",
"PinDetailResponse", "PinDetailResponse",

View File

@@ -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): class PinUpdate(BaseModel):
"""Schema for updating a staff PIN.""" """Schema for updating a staff PIN."""