บัตรสะสมแต้ม - แลกรางวัลและโค้ด
ภาพรวม
สอง endpoint ฝั่งลูกค้าที่เกี่ยวกับรางวัล ได้แก่ การขอ โค้ดคูปองอายุสั้น ของรางวัลที่ถืออยู่เพื่อให้ เครื่องพนักงานสแกน และการ ซื้อรางวัลด้วยแต้ม ซึ่งใช้ได้เฉพาะโหมด point
ความต่างของสองโหมดปรากฏชัดที่นี่: ในโหมดแสตมป์ รางวัลถูก mint ขึ้นจากการทำบัตรครบใบ ลูกค้าไม่ได้เลือก และไม่มีอะไรถูกหัก ส่วนในโหมด point ยอดแต้มทำหน้าที่เป็นสกุลเงิน ลูกค้าเลือกรางวัลจากบันไดแล้วจ่าย ค่าของมัน
Business Flow
ขอโค้ดคูปอง — POST /api/loyalty/:hash/rewards/:id/code (rate limit 20/60s)
resolve(c)→EnsureAccount→ListRewardsของ account- ค้นรางวัลตาม id ที่ขอ จากรายการของ account เท่านั้น รางวัลของผู้อื่นจึงไม่อยู่ในรายการและระบบ
ตอบ 404
LOYALTY_REWARD_INVALIDโดยไม่ยืนยันว่ามี id นั้นอยู่จริงหรือไม่ - สถานะที่ไม่ใช่
unclaimedตอบ 409LOYALTY_REWARD_CLAIMEDส่วนรางวัลที่หมดอายุแล้วตอบ 409LOYALTY_REWARD_EXPIRED - คืน
{code, title} - โค้ดจะ ไม่ปรากฏใน response ของหน้าบัตร โดยเจตนา เพราะ response ที่ถูกแคปหน้าจอเก็บไว้จะยัง ใช้แลกได้ตลอดกาล
ซื้อรางวัลด้วยแต้ม — POST /api/loyalty/:hash/rewards/redeem (rate limit 10/60s)
body {milestoneId} โดย rate limit เข้มกว่าเส้นทางอ่านเพราะขั้นตอนนี้หักยอด
ทั้งหมดรันใน transaction เดียว เพราะการหัก lot สำเร็จแต่ออกรางวัลไม่สำเร็จเท่ากับเอาแต้มลูกค้าไปโดยไม่ได้ อะไรกลับ ซึ่งเป็นความล้มเหลวเดียวที่กู้คืนไม่ได้หากไม่ขอโทษและแก้ด้วยมือ
milestoneIdที่เป็น 0 หรือน้อยกว่าตอบ 400LOYALTY_REWARD_INVALID- โปรแกรมไม่ live ตอบ 400
LOYALTY_PROGRAM_INACTIVEและหากไม่ใช่โหมด point ตอบ 400LOYALTY_NOT_POINT_MODEเพราะการปล่อยให้เส้นทางนี้รันในโหมดแสตมป์คือการ mint รางวัลโดยไม่ต้อง ทำบัตรครบ เท่ากับเสกรางวัลออกมาจากอากาศ EnsureAccountแล้วโหลด milestone ของ program id และ version ปัจจุบัน เนื่องจาก id จาก เวอร์ชันที่ถูกแทนที่ไปแล้วไม่ได้อยู่บนบันไดอีกต่อไป หากค้นด้วย id เพียงอย่างเดียว หน้าจอ LIFF ที่ค้าง อยู่จะซื้อรางวัลที่ถูกปลดไปแล้วได้ กรณีที่ไม่พบหรือrequired_unitsไม่มากกว่า 0 ตอบ 400LOYALTY_REWARD_INVALID- ด่าน tier — milestone ที่กำหนด
min_tier_rankมากกว่า 0 ต้องการให้ rank ของ account ถึงระดับ นั้น มิฉะนั้นตอบ 400LOYALTY_TIER_TOO_LOWการบังคับต้องอยู่ที่ server ไม่ใช่แค่ซ่อนปุ่มบน UI เพราะหน้าจอที่ค้างอยู่หรือ request ที่ทำขึ้นเองต้องซื้อรางวัลระดับ Gold บนบัญชี Silver ไม่ได้ ด่านนี้บังคับเฉพาะการแลกครั้งใหม่เท่านั้น รางวัลที่ mint ไปแล้วยังใช้ได้แม้ลูกค้าจะถูกลดชั้นภายหลัง เพราะเขาจ่ายไปแล้ว การยึดคืนเพราะหลุดจากหน้าต่างเวลาแบบ rolling เป็นสิ่งที่ปกป้องไม่ได้ - อ่าน lot ภายใน transaction ด้วย
LotsTxหากอ่านนอก transaction การกดปุ่มแลกสองครั้งจะเห็น ยอดเดียวกันและสำเร็จทั้งคู่ กรณียอดไม่พอตอบ 400LOYALTY_INSUFFICIENT_UNITS PlanBurnเผา lot ที่จะหมดอายุก่อน (FIFO ตามวันหมดอายุ) เพื่อให้ลูกค้าได้ใช้แต้มที่กำลังจะเสียไปอยู่แล้วConsumeLotหัก lot ทีละตัวแบบมีเงื่อนไข หากตัวใดคืนค่า false (มีการแลกอื่นแทรกระหว่างอ่านกับเขียน) ระบบจะ abort ทั้งหมด ไม่หักเพียงบางส่วน และหากยอดที่เผาได้ไม่ตรงกับที่ต้องการก็ abort เช่นกัน ในฐานะเข็มขัดนิรภัยชั้นที่สองInsertBurnเขียนแถว ledger ด้วยSourceRedeemInsertRewardสร้างรางวัลโดย ไม่ผูกกับบัตร เพราะรางวัลในโหมด point เป็นของ account ไม่ใช่ของ บัตร (คอลัมน์จึงเป็น nullable เพื่อรองรับกรณีนี้)- คืน
RewardViewของรางวัลที่เพิ่ง mint
ฝั่งพนักงาน
การ "เผา" รางวัลจริงหน้าร้านอยู่ที่ loyalty-staff ผ่าน POST /staff/redeem
ไฟล์และฟังก์ชันหลัก
| Route | Rate limit | Handler |
|---|---|---|
POST /api/loyalty/:hash/rewards/:id/code | 20/60s | internal/loyalty/handler.go → (*Handler).RewardCode |
POST /api/loyalty/:hash/rewards/redeem | 10/60s | (*Handler).RedeemPoints |
internal/loyalty/service_redeem.go—(*Service).RedeemPointsinternal/loyalty/repository.go—ListRewards,LotsTx,ConsumeLot,InsertBurn,InsertReward,FindRewardByID,ListMilestones,ListTiers,WithTxinternal/loyalty/entity.go—AvailableUnits,PlanBurn,rewardExpiryและ error codeLOYALTY_INSUFFICIENT_UNITS,LOYALTY_NOT_POINT_MODE,LOYALTY_TIER_TOO_LOW,LOYALTY_REWARD_INVALID/CLAIMED/EXPIREDinternal/loyalty/view.go—NewRewardViews
จุดเชื่อมต่อกับ Service อื่น
- ตารางฐานข้อมูล
loyalty.reward,loyalty.transaction(lot และ burn),loyalty.milestone,loyalty.tier,loyalty.account,loyalty.program - app-enabled-guard และ liff-authentication
- ที่มาของรางวัลในโหมดแสตมป์คือ loyalty-earn ส่วนการใช้รางวัลจริงหน้าร้านอยู่ที่ loyalty-staff และกฎ rank อยู่ที่ loyalty-tier
- ตรงกับ client-web feature:
loyalty-card(การแลกรางวัลและการแสดงโค้ดคูปองอายุสั้น)