促銷規則的型態一律是:(設定, CartState) → CartState。


上一篇講 pipeline 的形狀,這篇講裡面那些規則怎麼寫。

我的規定很簡單,每條促銷規則都是一個普通的靜態純函式,型態長得一模一樣:

public static function thresholdGift(ThresholdGift $promo, CartState $state): CartState
public static function couponCode(CouponDiscount $promo, CartState $state): CartState
public static function addOnOffer(AddOnOffer $promo, CartState $state): CartState

吃一份設定、吃目前的購物車狀態,回傳新的購物車狀態。不碰 DB、不碰 session、不碰 request、不寫 log。

這帶來三個好處。

第一,不符合條件就原樣回傳。

if ($state->linesSubtotal() < $promo->threshold) {
    return $state;
}

規則不需要知道自己排第幾個、後面還有誰。它只需要看一眼現在的狀態,決定要不要出手。這讓每條規則都可以單獨閱讀、單獨理解。

第二,折扣不是「改金額」,是「追加一筆調整」。

規則不會去把 total 減掉某個數字。它是 append 一筆 Adjustment:

return $state->withAdjustment(new Adjustment("滿 {$best->threshold} 折扣 {$offPercent}%", -$discount));

一個 label、一個金額,就這樣。購物車的應付金額永遠是「商品小計 + 所有調整的總和」,是推導出來的,不是被誰改出來的。

所以金額明細可以直接把 adjustments 逐筆列出來給客戶看:

商品小計            3,600
滿 3000 折扣 10%     -360
折扣碼 WELCOME 10%   -324

而且這份調整清單,落地的時候就是 order_discounts 的每一列。在記憶體裡我用負數方便相加,寫進 DB 的時候取絕對值存正整數,跟訂單金額欄位的規格一致。

第三,測試不用開資料庫。

促銷規則是電商最需要測試、也最容易算錯的地方。因為它是純函式,測試就是「準備一個 CartState、呼叫、斷言」。不用 factory、不用 seeder、不用 migration。

最後提一個實務上的小陷阱:折扣的基準要講清楚。我的折扣碼是這樣寫的:

$base = max(0, $state->linesSubtotal() + $state->adjustmentsTotal());

基準是「已經套用前面折扣之後」的商品金額。所以滿額 9 折之後再打折扣碼 9 折,結果是 81 折,是連乘、不是相加。這種事沒寫清楚,行銷跟工程師的認知一定會不一樣。我的做法是把它寫在那條規則的註解裡——因為那正是唯一決定這件事的地方。


⭐️ Shopify 網站開發服務(給品牌)
https://job.turn.tw/shopify-services

⭐️ 川川電商事務所(全客製電商開發服務)
https://chuanchuan.tw

⭐️ 台灣 Shopify 商家交流 LINE 群(非官方)
https://line.me/ti/g2/PZ_1LILWVWWuzZQ50HNpYA-A3k6QXWF6znqoBQ

⭐️ 台灣 Shopify 開發者 LINE 群(非官方)
https://line.me/ti/g2/YUasX5K3CJ4QdIx76zppjHlh3-q8w-xkSyK1LA

共有 0 則留言


⭐️ Shopify 網站開發服務(給品牌)
https://job.turn.tw/shopify-services

⭐️ 川川電商事務所(全客製電商開發服務)
https://chuanchuan.tw

⭐️ 台灣 Shopify 商家交流 LINE 群(非官方)
https://line.me/ti/g2/PZ_1LILWVWWuzZQ50HNpYA-A3k6QXWF6znqoBQ

⭐️ 台灣 Shopify 開發者 LINE 群(非官方)
https://line.me/ti/g2/YUasX5K3CJ4QdIx76zppjHlh3-q8w-xkSyK1LA
🏆 本月排行榜
🥇
站長阿川
📝45   💬1  
643
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次