Skip to main content

บัตรสะสมแต้ม - หน้าบัตรลูกค้า

ภาพรวม

endpoint ที่ส่งข้อมูล "บัตรทั้งใบ" ให้หน้า LIFF render ได้ภายใน round trip เดียว ประกอบด้วยโครง โปรแกรม (โหมดแสตมป์หรือแต้ม, ธีม, unit label), บัตรใบที่กำลังใช้งาน, บันไดรางวัล (milestone), รางวัลที่ลูกค้าถืออยู่, ยอดคงเหลือ, ระดับชั้น (tier) และระยะที่เหลือถึงชั้นถัดไป พร้อมกับมี endpoint สำหรับดูประวัติบัตรย้อนหลังแยกอีกตัวหนึ่ง

โปรแกรมสะสมแต้มมี 2 โหมดที่พฤติกรรมต่างกันมาก ได้แก่ stamp (บัตรตอกแสตมป์ เมื่อครบใบจะได้รางวัล อัตโนมัติ) และ point (แต้มทำหน้าที่เป็นสกุลเงิน ลูกค้าเลือกแลกรางวัลเอง)

Business Flow

โหลดหน้าบัตร — GET /api/loyalty/:hash

  1. resolve(c) ค้น OA จากพารามิเตอร์ :hash (ไม่พบตอบ 404 "loyalty card not found") แล้ว verify LIFF token กับ channel ของ OA นั้นผ่าน ladder ชุดเดียวกับฝั่ง bulletin
  2. โหลดโปรแกรมที่ live ของ OA โดยกรณีที่ไม่มีโปรแกรมไม่ถือเป็น error ระบบจะคืน response ที่มีเพียง ชื่อและรูปของ OA (milestones: [], rewards: []) เพื่อให้ LIFF แสดงหน้าจอ "ยังไม่เปิดให้บริการ"
  3. EnsureAccount สร้างแถว loyalty.account ให้ LINE user รายนี้ถ้ายังไม่มี
  4. โหลดบัตรทั้งหมดของ account แล้วเลือกใบที่มีสถานะ active มาเป็น card
  5. โหลด milestone ของ program id และ version นั้นโดยเฉพาะ เนื่องจากโปรแกรมมีระบบ versioning บันไดรางวัลของเวอร์ชันเก่าจึงไม่ถูกนำมาแสดง
  6. โหลดรางวัลของ account แล้ว render ผ่าน NewRewardViews(rewards, storage, now) ซึ่งตัดสินสถานะ หมดอายุจากเวลาปัจจุบัน
  7. คำนวณ Balance จาก AvailableUnits(lots, now) โดยนับเฉพาะ ledger lot ที่ยังไม่หมดอายุ
  8. ส่วนของ tier (เฉพาะโหมด point ที่เปิด tier_enabled) คำนวณสดทุกครั้งที่โหลดบัตร ไม่ cache ไว้ บน account เพราะบันไดชั้นถูก re-price ใน CMS ได้ตลอดเวลา และชื่อ tier ที่ cache ไว้จะยังแสดงชั้นที่ ถูกเปลี่ยนชื่อหรือลบไปแล้ว
    • ชั้นปัจจุบันถูกอ่านด้วย tier_id โดยตรง เพราะ tier ไม่ได้ version ตามโปรแกรม (ผูกกับ OA) ถ้าไป match กับ rung ของ program version ปัจจุบัน ลูกค้าที่ได้ชั้นมาจากเวอร์ชันก่อนหน้าจะไม่มีชั้นเลย
    • บันไดทั้งหมดคือ tier ที่ active ของ OA (ข้าม program version) ด้วยเหตุผลเดียวกัน
    • NextTier คือ rung ที่ active และมี rank สูงกว่าชั้นปัจจุบันโดยใกล้ที่สุด
    • SpendToNext คำนวณจาก nextSpendTarget(next) ลบด้วยยอด spend ในหน้าต่างเวลา (ไม่ต่ำกว่า 0) โดยหน้าต่างเวลาเริ่มต้นที่ 3 เดือน ทั้งนี้ tier ที่ไม่มีกฎ metric spend จะได้ target เป็น 0 ซึ่งหมายถึงไม่มีตัวเลขบาทให้นับถอยหลัง ระบบจะไม่คิดตัวเลขขึ้นมาเอง
  9. รูปทั้งหมด (cover, logo, รูปรางวัล, รูปพื้นหลังของ tier) ถูก resolve เป็น public URL ผ่าน publicURLOf หาก path ว่างหรือไม่มี storage ระบบจะไม่ส่งรูปออกไป แทนที่จะส่ง URL ที่ชี้ไปยัง bucket root

ประวัติบัตร — GET /api/loyalty/:hash/cards

เรียก EnsureAccount แล้วคืน {cards: [...]} ทั้งหมด เป็นหน้าประวัติบัตรที่ LINE เองไม่มีให้

สิ่งที่ไม่อยู่ใน response

โค้ดคูปองของรางวัลไม่ถูกส่งมากับหน้าบัตรโดยเจตนา ดูรายละเอียดที่ loyalty-reward-redeem เพราะหากใส่มาใน card response ภาพหน้าจอที่ ถูกแคปไว้จะใช้แลกได้ตลอดกาล

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

RouteHandler
GET /api/loyalty/:hashinternal/loyalty/handler.go(*Handler).GetCard
GET /api/loyalty/:hash/cards(*Handler).ListCards
  • internal/loyalty/register.goRegister(r, deps) พร้อม appguard.AppEnabledGuard(db,"loyalty")
  • internal/loyalty/handler.go(*Handler).resolve และ type caller
  • internal/loyalty/auth.go — ladder สำหรับ verify token (สำเนาของฝั่ง bulletin)
  • internal/loyalty/view.goCardResponse, NewProgramView, NewCardView, NewCardViews, NewMilestoneViews, NewRewardViews, NewTierView, NewBranchViews, publicURLOf
  • internal/loyalty/repository.goFindOaByHash, FindLiveProgram, EnsureAccount, ListCards, ListMilestones, ListRewards, Lots, GetTierByID, ListTiersByLineOa, SpendInWindow
  • internal/loyalty/entity.goProgram, Account, CardInstance, Milestone, Reward, AvailableUnits, PlanBurn, CardExpiry, CooldownBlocks, UnitsForSpend และค่าคงที่ ModeStamp/ModePoint, CardActive/CardComplete/CardExpired, RewardUnclaimed/RewardClaimed/RewardExpired, MaxCardSize=20, MaxEarnUnits=10000, TokenTTL=60s

จุดเชื่อมต่อกับ Service อื่น

  • ตารางฐานข้อมูล loyalty.program, loyalty.account, loyalty.card_instance, loyalty.milestone, loyalty.reward, loyalty.transaction (ledger lot), loyalty.tier, line_oa
  • internal/storagex สำหรับสร้าง public URL ของรูปทั้งหมด
  • app-enabled-guard (appID loyalty) และ liff-authentication
  • loyalty-tier เป็นแหล่งกฎการคำนวณชั้น ส่วน loyalty-earn และ loyalty-reward-redeem เป็นเส้นทางที่เปลี่ยนแปลงข้อมูลบนหน้าบัตร
  • ตรงกับ client-web feature: loyalty-card