Skip to main content

บัตรสะสมแต้ม - แลกรางวัลและโค้ด

ภาพรวม

สอง endpoint ฝั่งลูกค้าที่เกี่ยวกับรางวัล ได้แก่ การขอ โค้ดคูปองอายุสั้น ของรางวัลที่ถืออยู่เพื่อให้ เครื่องพนักงานสแกน และการ ซื้อรางวัลด้วยแต้ม ซึ่งใช้ได้เฉพาะโหมด point

ความต่างของสองโหมดปรากฏชัดที่นี่: ในโหมดแสตมป์ รางวัลถูก mint ขึ้นจากการทำบัตรครบใบ ลูกค้าไม่ได้เลือก และไม่มีอะไรถูกหัก ส่วนในโหมด point ยอดแต้มทำหน้าที่เป็นสกุลเงิน ลูกค้าเลือกรางวัลจากบันไดแล้วจ่าย ค่าของมัน

Business Flow

ขอโค้ดคูปอง — POST /api/loyalty/:hash/rewards/:id/code (rate limit 20/60s)

  1. resolve(c)EnsureAccountListRewards ของ account
  2. ค้นรางวัลตาม id ที่ขอ จากรายการของ account เท่านั้น รางวัลของผู้อื่นจึงไม่อยู่ในรายการและระบบ ตอบ 404 LOYALTY_REWARD_INVALID โดยไม่ยืนยันว่ามี id นั้นอยู่จริงหรือไม่
  3. สถานะที่ไม่ใช่ unclaimed ตอบ 409 LOYALTY_REWARD_CLAIMED ส่วนรางวัลที่หมดอายุแล้วตอบ 409 LOYALTY_REWARD_EXPIRED
  4. คืน {code, title}
  5. โค้ดจะ ไม่ปรากฏใน response ของหน้าบัตร โดยเจตนา เพราะ response ที่ถูกแคปหน้าจอเก็บไว้จะยัง ใช้แลกได้ตลอดกาล

ซื้อรางวัลด้วยแต้ม — POST /api/loyalty/:hash/rewards/redeem (rate limit 10/60s)

body {milestoneId} โดย rate limit เข้มกว่าเส้นทางอ่านเพราะขั้นตอนนี้หักยอด

ทั้งหมดรันใน transaction เดียว เพราะการหัก lot สำเร็จแต่ออกรางวัลไม่สำเร็จเท่ากับเอาแต้มลูกค้าไปโดยไม่ได้ อะไรกลับ ซึ่งเป็นความล้มเหลวเดียวที่กู้คืนไม่ได้หากไม่ขอโทษและแก้ด้วยมือ

  1. milestoneId ที่เป็น 0 หรือน้อยกว่าตอบ 400 LOYALTY_REWARD_INVALID
  2. โปรแกรมไม่ live ตอบ 400 LOYALTY_PROGRAM_INACTIVE และหากไม่ใช่โหมด point ตอบ 400 LOYALTY_NOT_POINT_MODE เพราะการปล่อยให้เส้นทางนี้รันในโหมดแสตมป์คือการ mint รางวัลโดยไม่ต้อง ทำบัตรครบ เท่ากับเสกรางวัลออกมาจากอากาศ
  3. EnsureAccount แล้วโหลด milestone ของ program id และ version ปัจจุบัน เนื่องจาก id จาก เวอร์ชันที่ถูกแทนที่ไปแล้วไม่ได้อยู่บนบันไดอีกต่อไป หากค้นด้วย id เพียงอย่างเดียว หน้าจอ LIFF ที่ค้าง อยู่จะซื้อรางวัลที่ถูกปลดไปแล้วได้ กรณีที่ไม่พบหรือ required_units ไม่มากกว่า 0 ตอบ 400 LOYALTY_REWARD_INVALID
  4. ด่าน tier — milestone ที่กำหนด min_tier_rank มากกว่า 0 ต้องการให้ rank ของ account ถึงระดับ นั้น มิฉะนั้นตอบ 400 LOYALTY_TIER_TOO_LOW การบังคับต้องอยู่ที่ server ไม่ใช่แค่ซ่อนปุ่มบน UI เพราะหน้าจอที่ค้างอยู่หรือ request ที่ทำขึ้นเองต้องซื้อรางวัลระดับ Gold บนบัญชี Silver ไม่ได้ ด่านนี้บังคับเฉพาะการแลกครั้งใหม่เท่านั้น รางวัลที่ mint ไปแล้วยังใช้ได้แม้ลูกค้าจะถูกลดชั้นภายหลัง เพราะเขาจ่ายไปแล้ว การยึดคืนเพราะหลุดจากหน้าต่างเวลาแบบ rolling เป็นสิ่งที่ปกป้องไม่ได้
  5. อ่าน lot ภายใน transaction ด้วย LotsTx หากอ่านนอก transaction การกดปุ่มแลกสองครั้งจะเห็น ยอดเดียวกันและสำเร็จทั้งคู่ กรณียอดไม่พอตอบ 400 LOYALTY_INSUFFICIENT_UNITS
  6. PlanBurn เผา lot ที่จะหมดอายุก่อน (FIFO ตามวันหมดอายุ) เพื่อให้ลูกค้าได้ใช้แต้มที่กำลังจะเสียไปอยู่แล้ว
  7. ConsumeLot หัก lot ทีละตัวแบบมีเงื่อนไข หากตัวใดคืนค่า false (มีการแลกอื่นแทรกระหว่างอ่านกับเขียน) ระบบจะ abort ทั้งหมด ไม่หักเพียงบางส่วน และหากยอดที่เผาได้ไม่ตรงกับที่ต้องการก็ abort เช่นกัน ในฐานะเข็มขัดนิรภัยชั้นที่สอง
  8. InsertBurn เขียนแถว ledger ด้วย SourceRedeem
  9. InsertReward สร้างรางวัลโดย ไม่ผูกกับบัตร เพราะรางวัลในโหมด point เป็นของ account ไม่ใช่ของ บัตร (คอลัมน์จึงเป็น nullable เพื่อรองรับกรณีนี้)
  10. คืน RewardView ของรางวัลที่เพิ่ง mint

ฝั่งพนักงาน

การ "เผา" รางวัลจริงหน้าร้านอยู่ที่ loyalty-staff ผ่าน POST /staff/redeem

ไฟล์และฟังก์ชันหลัก

RouteRate limitHandler
POST /api/loyalty/:hash/rewards/:id/code20/60sinternal/loyalty/handler.go(*Handler).RewardCode
POST /api/loyalty/:hash/rewards/redeem10/60s(*Handler).RedeemPoints
  • internal/loyalty/service_redeem.go(*Service).RedeemPoints
  • internal/loyalty/repository.goListRewards, LotsTx, ConsumeLot, InsertBurn, InsertReward, FindRewardByID, ListMilestones, ListTiers, WithTx
  • internal/loyalty/entity.goAvailableUnits, PlanBurn, rewardExpiry และ error code LOYALTY_INSUFFICIENT_UNITS, LOYALTY_NOT_POINT_MODE, LOYALTY_TIER_TOO_LOW, LOYALTY_REWARD_INVALID/CLAIMED/EXPIRED
  • internal/loyalty/view.goNewRewardViews

จุดเชื่อมต่อกับ 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 (การแลกรางวัลและการแสดงโค้ดคูปองอายุสั้น)