訂單符合回饋活動後,系統先向 PCPS 保留一筆額度。線上訂單要等付款結果,才能決定繼續給獎或返還;線下訂單則在回饋流程中完成佔額

「佔額」是回饋活動在 PromoCodePool Service(PCPS) 的 Group 中,先替符合條件的訂單會員分派並兌換一筆 Code,使該筆回饋占用活動限額。這是配額操作,不是給點或給券。Promotion Worker 另外在 DynamoDB(下稱 DDB)存回饋主單及子單狀態,藉此決定後續是否能回饋、取消及稽核

本專案支援三種相關活動類型:RewardReachPriceWithPoint2、RewardReachPriceWithRatePoint2、RewardReachPriceWithCoupon,分派器、Job 的 IsQuotaPromotion 使用由 GroupCode 判斷

OrderPlaced:建立新的佔額 NMQ Task

下單後,誰真正向 PCPS 佔額?

分派器只建立 Task。後續 Job 試算命中活動,才會呼叫 PCPS;IsMatch 與佔額成功是兩個判斷。

OrderFailed:查出已佔額的 Code 並返還

付款失敗後,依 DDB 狀態決定返還

OrderFailed 在分派器內直接處理返還。查不到紀錄或遇到未預期狀態會報錯。

OrderFailed 在同一個 Dispatcher 內直接走返還流程,不會建立上述佔額 NMQ Task。只有 DDB 主單為 Occupy 才以 GroupCode + TG 查 PCPS 的 CodeId,呼叫 reverse-redeemed-and-dispatched,成功後更新 DDB 為 Cancel;給券活動還會更新取消券數量。這裡的 reverse-redeemed 是返還已佔額度,與 OrderPlaced 後 Job 執行的 dispatch-and-redeem 是相反操作

OrderCreated 的後續回饋分派與 Occupy → WaitToReward 轉換,見「何時佔額:線上與線下」分頁;它不是 PromotionRewardOccupyDispatcherJob 的分支

佔額資料在服務間如何流動

這張圖表達責任與資料去向;實際先後順序由後面的線上、線下流程圖說明。
概念/服務 職責 關鍵資料
Promotion Service / Engine 查訂單時間有效的活動與活動規則;試算是否符合活動 PromotionEngineId、活動類型、GroupCode、IsMatch
NMQv3 OrderPlaced 分派器逐活動建立點數/券佔額 Task;OrderCreated 另建回饋 Task;OrderFailed 在佔額分派器內直接處理返還 JobName、Data、EventSource
PCPS 真正分派、兌換、查詢與返還配額 Code GroupId/GroupCode、CodeId、ReceiverId、Labels、State
DDB 與 Locker 保存回饋主/子單及歷程;以訂單+活動鍵鎖住並行處理 Occupy、WaitToReward、MatchWithoutQuota、Cancel
點數中心/券服務 在回饋條件與 BookingTime 成立時實際發點/發券 Reward、明細、已給/回收數量
稽核器 對照 DDB 回饋狀態和 PCPS 實際佔額筆數 期望 0 或 1 筆

Group 先建立;訂單再佔用其中一筆 Code

Group 是活動的額度規則;Code 是單次佔額結果。GroupCode 串起建立與訂單佔額兩階段。

PCPS(Promo Code Pool Service)以 Group 保存活動的佔額規則,以 Code 表示一次已分派的額度。Promotion Web API 建立活動時負責建立並啟用 Group;Worker 在訂單符合活動後,才對該 Group 呼叫 dispatch-and-redeem。建立群組得到的 Id 會寫成活動的 PromotionEngine_GroupCode,Worker 取得規則後則可從 Rule-Record 的 GroupCode 找到同一群組

建立/更新 Group 的參數

PCPS 欄位 Promotion Web API 傳入值 在回饋活動的意義
ModeType Functional 用多少才產生多少 Code;不是先匯入整批代碼。
DispatchType Random 搭配 Functional 時,依代碼產生規則即時產生 Code;此處的 Random 不是「隨機決定哪筆訂單中活動」。
Pattern GUID 產生 Code 使用的格式設定。
Since 活動的 StartDateTime 群組開始時間。
Until DateTime.MaxValue 群組不隨活動的 EndDateTime 到期,讓活動結束後補處理舊訂單時仍可使用該群組;活動本身是否有效仍由活動時間與規則判斷。
LimitRuleSettings[].ActionType Redeem 限額規則套用在兌換動作。
LimitRuleSettings[].LimitTarget 2 PCPS 的會員限額維度;Web API 程式碼明註 2 代表會員。
LimitRuleSettings[].LimitRules[].Quota 活動的 Quota 上述會員維度的兌換額度設定。Quota <= 0 時,建立群組方法不接受該設定。
CanRedeemDirectly true 允許直接兌換;Worker 使用的 dispatch-and-redeem 會在同一次呼叫中分派並兌換 Code。

Web API 透過 POST api/groups 建立、PATCH api/groups/{id}/enable 啟用,更新時用 PUT api/groups/{id}

建立 Group 實例。 假設商店 1001 建立一檔回饋活動,活動期間為 2025-05-01 00:00 至 2025-05-31 23:59(台灣時間),設定 Quota=1。Promotion Web API 會依活動資料組成下列 PCPS request。商店與活動設定是示意值;欄位及 Until=DateTime.MaxValue 對應 PromoCodePoolService.BuildQuotaGroupRequestEntity 的實際程式碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// POST api/groups;此處以 C# 表示 Web API 組出的 request,非 HTTP 原始 JSON。
new GroupCreateOrUpdateRequestEntity
{
ModeType = "Functional",
DispatchType = "Random",
Pattern = "GUID",
Since = new DateTimeOffset(2025, 5, 1, 0, 0, 0, TimeSpan.FromHours(8)),
Until = DateTime.MaxValue,
LimitRuleSettings = new[]
{
new LimitRuleSettingEntity
{
ActionType = "Redeem",
LimitTarget = 2, // 會員
LimitRules = new[] { new LimitRuleEntity { Quota = 1 } }
}
},
CanRedeemDirectly = true
};

建立成功後,Web API 取回 PCPS 回應的 Id,再呼叫 PATCH api/groups/{id}/enable。假設回傳 Id=6f114aae-3391-4e0d-84cc-fdb3ed68e87a,活動會把這個字串存成 PromotionEngine_GroupCode,後續訂單就用它指定要佔額的 Group。此 ID 僅為串連範例的示意值,程式碼沒有保證 Group ID 一定採用 GUID 格式。雖然活動在 5 月底結束,Group 的 Until 仍為 DateTime.MaxValue;訂單是否屬於活動期間,仍需按訂單時間與活動規則判斷。

每筆訂單佔額的參數

欄位 Worker 傳入值與用途
URL 的 groupId 活動的 GroupCode,指定要佔用哪個 PCPS Group。
ReceiverId 會員 ID 的字串,指定接收這筆 Code 的會員。
Tags 空集合。Worker request model 說明 Tags 用來形成限額鍵、不能拿來查詢;此流程沒有額外傳入 Tag。
Labels 放訂單彙總碼 TG,作為可查詢註記。後續以 GroupCode + TG 呼叫 get-by-label 找出該訂單的 Code。該欄位的 model 註明長度上限為 180 bytes,且不得包含 @ 或 |。
HTTP headers ny-market 是市場、ny-shop-id 是商店 ID、ny-source-type 固定為 PromotionReward;Worker 佔額時的 ny-user-id 是會員 ID。Web API 建/改 Group 時的 ny-user-id 則來自操作使用者名稱。

線下訂單在查 DDB/PCPS 時使用 CrmSalesOrderCodePrefix + CrmSalesOrderId 作為訂單代碼。PCPS 回傳的單筆 CodeId 用於日後返還;HTTP 成功不等於佔額成功,Worker 還會確認回傳 State=Redeemed

線上訂單佔額實例。 沿用上面的 Group,假設會員 2002 在 2025-05-12 下了一筆線上訂單 TG250512P00001,OrderPlaced 佔額 Job 試算後確認 IsMatch=true。這個 TG 樣態取自專案測試資料;商店、會員及 Group ID 仍是示意值。Worker 會送出:

1
2
3
4
5
6
7
8
9
10
11
12
POST api/groups/6f114aae-3391-4e0d-84cc-fdb3ed68e87a/dispatch-and-redeem
ny-market: TW
ny-shop-id: 1001
ny-source-type: PromotionReward
ny-user-id: 2002
Content-Type: application/json

{
"receiverId": "2002",
"tags": [],
"labels": ["TG250512P00001"]
}

這裡的 URL 指向活動 Group,receiverId 指向會員,labels 把 Code 與 TG 關聯起來,tags 沒有額外值。若 PCPS 回傳 State=Redeemed,Worker 視為佔額成功,線上 DDB 主單記為 Occupy;之後可用相同 GroupCode 與 Label=TG250512P00001 呼叫 get-by-label 查回該 Code。

線下訂單佔額實例。 同一檔活動若也適用線下訂單,假設另一位會員 2003 的 CrmSalesOrderId=987654321 在活動期間符合回饋條件。線下回饋 Job 會把 CRM 訂單 ID 組成 CrmSalesOrder:987654321,將它當作 PCPS Label;CrmSalesOrder: 是 OthersCrmSalesOrderPromotionRewardCalculateService.CrmSalesOrderCodePrefix 的實際常數值。線下不會先等 OrderPlaced 佔額 Job,而是在回饋 Job 試算命中後送出:

1
2
3
4
5
6
7
8
9
10
11
12
POST api/groups/6f114aae-3391-4e0d-84cc-fdb3ed68e87a/dispatch-and-redeem
ny-market: TW
ny-shop-id: 1001
ny-source-type: PromotionReward
ny-user-id: 2003
Content-Type: application/json

{
"receiverId": "2003",
"tags": [],
"labels": ["CrmSalesOrder:987654321"]
}

線下查詢時也用該 Group ID 與 Label=CrmSalesOrder:987654321 呼叫 get-by-label。若 PCPS 回傳 State=Redeemed,線下 DDB 主單直接進入 WaitToReward;同一範例使用不同會員,是因為 Group 設定的 Quota=1 套用在會員限額維度。兩種訂單的 request 欄位與成功判斷都對應點數/券 Job 的 ProcessOccupyQuotaAsync

線上與線下如何形成查詢 Label

兩條路徑使用同一種 PCPS 查詢方式,差在 Label 的來源與格式。
操作 Worker 呼叫 重要語意
佔額 POST api/groups/{groupId}/dispatch-and-redeem 成功 HTTP 回應仍需 State=Redeemed 才算佔額成功
查 TG 佔額 GET api/codes/get-by-label?GroupId=...&Label=...&Limit=30 Labels 是查詢註記;GetMemberPromotionQuotaInfoByTradesOrderGroup 要求恰好一筆
返還 POST api/codes/{codeId}/reverse-redeemed-and-dispatched 以 GroupId、ReceiverId 返還;OrderFailed 使用單筆返還,逆流程 helper 可逐筆返還
失敗/例外 程式處理 影響
佔額 HTTP 409 轉 InsufficientQuotaException,不重試 Job 回 isOccupy=false,符合活動者記 MatchWithoutQuota
佔額網路/其他 HTTP 錯誤 Client 有有限次重試;仍失敗拋例外 Job 包為可釋放鎖的失敗;不應解讀成「額度不足」
佔額成功但寫 DDB 失敗 PCPS 與 DDB 分開呼叫 可能留下 PCPS 已兌換、DDB 缺漏;這是由呼叫順序推得的一致性風險,需靠查 PCPS 與任務日誌確認個案
OrderFailed 找不到 DDB/PCPS Code DDB 查詢有限次重試;單筆查不到或非恰好一筆會拋錯 不會默默把 DDB 寫 Cancel
逆流程 ReverseOccupyQuotaAsync 查無/未兌換 容忍並結束;Timeout/TaskCanceled 最多三次 這與 OrderFailed 的嚴格單筆返還路徑不同

注意:PromotionRewardOccupyDispatcherJob 有 OrderFailedDelaySeconds=30 常數與「增加等待時間」註解,但目前 ProcessQuotaReverseRedeemed 呼叫前**沒有使用該常數或 Task.Delay**。該路徑以 DDB 讀取/鎖的重試應對先後順序,不宜宣稱實際固定等待 30 秒

活動與 PCPS Group 使用兩條時間界線

活動以訂單時間判斷;PCPS Group 另有自己的 Since/Until。圖中的「可嘗試」仍須有額度且 PCPS 回傳成功。

回饋活動的有效期和 PCPS 佔額群組的有效期是兩組設定。活動規則的 Since、Until 分別採用活動的 StartDateTime、EndDateTime,用來判斷訂單在下單當時是否符合活動時間。建立 PCPS 群組時,Since 同樣採用活動開始時間,但 Until 固定為 DateTime.MaxValue,沒有跟著活動結束時間到期。因此,活動已結束後,只要補跑流程仍能找到該活動與訂單,過去符合活動期間的訂單仍有機會在現在向 PCPS 佔額

情境 活動時間判斷 PCPS 群組時間判斷 結果
現在補跑活動期間內的舊訂單 訂單時間落在活動開始與結束之間 群組的 Until 尚未到期 時間設定不會阻止佔額,仍須符合活動條件且有額度
現在處理活動開始前的訂單 訂單時間早於活動開始 即使群組已存在 訂單不符合活動時間,不應進入該活動佔額
現在提前處理未來活動的訂單 訂單時間可能落在未來活動期間 處理當下早於群組 Since 依群組設定不應提前佔額;PCPS 的實際拒絕行為仍須以 PCPS 實作確認

補跑的界線:Group 不隨活動結束而過期,讓舊訂單仍有機會佔額;一般 Worker 的候選活動查詢範圍仍會影響能否補跑。

建立活動時,backend API 只對支援佔額的回饋類型且 Quota > 0 的活動建立並啟用 PCPS 群組,取得 GroupId 後寫入 PromotionEngine_GroupCode。Quota = 0 代表不建立佔額群組。活動開始後,更新流程會直接跳過佔額設定變更

這個時間設定解決的是 PCPS 群組過期問題,不保證任意久以前的活動都能由一般 Worker 分派器找出。目前該分派器的候選活動查詢從現在往前七天開始,再用訂單時間過濾;超過查詢範圍的補跑仍需檢查實際入口

佔額更新

活動尚未開始且有佔額設定時,backend API 可以更新群組的額度與開始時間;活動開始後,佔額設定更新流程會直接返回。

資料表

PromotionEngine_GroupCode 保存 PCPS 回傳的 Group ID,供 Worker 從活動規則取得後佔額。

線上:下單先佔額,付款後才進入待回饋

圖分開標示 Task 建立與 Job 執行;PCPS 成功、未成功的 DDB 狀態在下方條件表說明。

線上訂單(ECom)

  1. OrderPlaced 到達佔額分派器。它按訂單時間選出有效的三類回饋活動,保留有 GroupCode 的活動;點數活動還需 ExtendInfo 有值。每檔活動各建一個點數 V2 或券 Job,要求的 EventSource=OrderPlaced。分派本身尚未扣額度
  2. Job 取得同一筆 PromotionRuleRecord,做通路檢查和回饋試算。在 DDB key 上取鎖;沒有既有紀錄、IsMatch=true 才向 PCPS 呼叫 dispatch-and-redeem。回傳 State=Redeemed 才當作成功;配額不足則記為失敗。之後寫入 DDB 主/子單。既有紀錄則直接返回,以 DDB 作為重跑的防護之一
  3. OrderCreated 進入另一個分派器。對有 GroupCode 的活動,它把回饋 Job 的 BookingTime 設成 UtcNow + 5 分 10 秒,試圖讓佔額流程先完成。回饋 Job 若查不到線上佔額活動的 DDB 紀錄,會拋錯而不直接給點/券;查到 Occupy 才建立績效 Job 並轉 WaitToReward。到真正回饋時間前不執行給點/券

線下訂單(Others)

線下回饋 Job 若尚無 DDB 紀錄,會直接試算;符合活動且有 GroupCode 時在同一次回饋流程呼叫 PCPS 佔額,再插入 DDB。成功的線下紀錄初始即為 WaitToReward,不會經過線上訂單專用的 Occupy 暫存狀態

線下:佔額與待回饋在同一個 Job

線下沒有線上的 OrderPlaced 暫存階段;已有 DDB 紀錄時不沿著「首次試算」路徑重建佔額。

績效的兩個時點:回饋 Job 先建立 PromotionRewardLoyaltyPointsPerformanceV2 NMQ Task;該績效 Job 後續才非同步寫入 PromotionEnginePerformance。Task 已建立不表示資料表已寫入。

線上與線下何時建立績效 Task

線上在 OrderCreated 回饋流程讀到 Occupy 時建立 Task;線下在同一次回饋 Job 試算並佔額成功後建立 Task。寫表由另一個 Job 完成。
訂單來源 佔額成功後 建立績效 Task 的時機
線上 OrderPlaced 記為 Occupy 後續 OrderCreated 回饋 Job 讀到 Occupy,建立 Task 並轉成 WaitToReward。
線下 同一回饋 Job 試算、佔額,成功後進入 WaitToReward 同一回饋 Job 建立績效 Task。

建立績效 Task 不必等點數或券實際發出。若線上訂單在付款前從 Occupy 取消,正常流程不建立績效紀錄;若已轉為 WaitToReward 後才取消,績效稽核仍會檢查其紀錄。

中活動與佔額是兩層判斷

完整例外與後續分支在下表。流程先試算,再向 PCPS 嘗試佔額。
條件 PCPS 動作 DDB 主單結果 後續
沒有 GroupCode 無 中活動為 WaitToReward,未中為 Unmatch 一般回饋路徑
有 GroupCode,試算未中 無 Unmatch 不給點/券
有 GroupCode,試算中,PCPS 回 Redeemed,線上 OrderPlaced 分派並兌換 1 Code Occupy 等 OrderCreated 轉狀態
有 GroupCode,試算中,PCPS 回 Redeemed,線下 分派並兌換 1 Code WaitToReward 按 BookingTime 回饋
有 GroupCode,試算中,額度不足或回傳狀態非 Redeemed 佔額未確認成功 MatchWithoutQuota 不進入回饋
線上回饋 Job 無 DDB 紀錄 無 不建立 拋錯,待前段佔額資料釐清
線上回饋 Job 讀到 Occupy 無新佔額 Occupy → WaitToReward 建績效 Job,後續給點/券
OrderFailed 且 DDB 為 Occupy 查 Code 並返還 Occupy → Cancel 券另增加取消數量

DDB 主單狀態如何轉換

Occupy 是線上付款前的暫存狀態;線下佔額成功直接進入 WaitToReward。

Reward 等狀態的後續逆流程有多種分支;上圖呈現與佔額直接相關的主要轉換,不代表每種回收情境都必定直接寫 Cancel。點數與券插入紀錄的狀態判斷一致

為什麼要有獨立的 Occupy 狀態?

線上訂單收到 OrderPlaced 時,訂單已成立但尚未付款。此時先替符合活動條件的訂單保留額度,並把 DDB 主單記為 Occupy。它表達的是「PCPS 已成功佔額,付款結果仍待確認」,因此不會把這筆紀錄直接當成可以給點或給券的 WaitToReward。程式中的 RewardStatusEnum.Occupy 也明確註明這是訂單成立但未付款的暫時狀態

獨立狀態讓後續流程有清楚的分流依據

  1. 付款完成後才進入待回饋。 回饋 Job 讀到 Occupy,才將主單改為 WaitToReward,再依回饋時間執行給點或給券。佔額成功本身不等於獎勵已可發放
  2. 付款失敗時能精準返還。 OrderFailed 只對 Occupy 執行 PCPS 返還並更新為 Cancel;Unmatch、MatchWithoutQuota、Cancel 都不需要返還。這避免把「有中活動但沒佔到額度」誤當成已佔額
  3. 能辨認卡住的中間階段。 稽核會檢查 Occupy 是否仍對應一筆 PCPS 佔額,也會指出訂單已非待付款、DDB 卻仍停在 Occupy 的情況,方便追查後續回饋 Job 是否漏執行

OrderFailed 的返還順序

OrderFailed 在分派器內直接返還;一般取消、退款與重算有另一條逆流程。

OrderFailed 走佔額分派器的另一分支:以訂單時間再找有 GroupCode 的活動,排除純線下活動,依活動類型取得點數/券 DDB 紀錄。只有 RewardStatus=Occupy 才返還;Unmatch、MatchWithoutQuota、Cancel 略過,其餘狀態當異常。返還先從 PCPS 以 TG 查到單筆 Code,再呼叫 Reverse;成功後把主/子單改 Cancel,券另外更新道具表 CanceledCouponQty

訂單後續取消、退款或重算也可能返還已佔額資源。點數流程在重算後不符合活動時呼叫 ReverseOccupyQuotaAsync;券流程在整單取消,或重算應給券 0 且尚未給券時呼叫它。該 helper 以 TG 查可能多筆 Code,逐筆 Reverse,查無/未兌換視為可容忍,逾時有限次重試。返還時機不限付款失敗

一般逆流程:觸發條件

取消、退款或重算後不再符合活動,依點數或券的規則判定是否應返還。

查詢與逐筆返還

ReverseOccupyQuotaAsync 依訂單 Label 查 Code,對查到的紀錄逐筆呼叫 Reverse。

容忍情境

查無 Code 或 Code 尚未兌換時,helper 可結束;逾時則依有限次重試處理。

稽核有正向回饋與逆流程回收兩條 Job 鏈路。兩者都會碰到佔額,但檢查目的不同:正向確認應保留的額度存在、不得保留的額度不存在;逆流程確認取消或回收後,額度確實返還。

正向稽核:DDB 狀態與 PCPS 筆數

分派器還會依訂單來源選線上或線下 Job;圖中縮短名稱,完整名稱列於下表。

逆流程稽核:返還後是否留有額度

點數與券的返還檢查條件不同;線上與線下的完整 Job 名稱列於下表。
鏈路 分派與執行 Job 佔額相關檢查
正向回饋稽核 AuditPromotionRewardLoyaltyPointsDispatchV2 依訂單來源及活動類型,分派 AuditPromotionRewardLoyaltyPointsV2/AuditPromotionRewardCoupon,或線下的 AuditCrmOthersOrderPromotionRewardLoyaltyPointsV2/AuditCrmOthersOrderPromotionRewardCoupon。四個 Job 都註冊 PromotionRewardQuotaAuditor。 以 DDB 回饋主單狀態對照 PCPS 查到的佔額筆數
逆流程回收稽核 AuditPromotionRecycleDispatch 分派 AuditPromotionRecycleLoyaltyPoints/AuditPromotionRecycleCoupon,或對應的線下回收稽核 Job。點數與券分別使用 PromotionRecycleQuotaPointsRecordAuditor、PromotionRecycleQuotaCouponRecordAuditor。 檢查整單取消、DDB Cancel 或點數全回收後,PCPS 是否仍留有額度

線上正向分派器先找待稽核訂單,再依訂單時間區間找回饋活動並建立逐筆稽核 Task。線下批次分派器會另建線下正向與逆流程稽核 Task,BookingTime 分別設為建立當下加 6 小時 30 分及 10 小時;這是排程稽核,不是每次 dispatch-and-redeem 成功就立即稽核

正向佔額筆數規則。 PromotionRewardQuotaAuditor 只對規則有 GroupCode 的活動檢查。它按訂單來源決定 TG:線上用原 TG,線下加 CrmSalesOrderCodePrefix;再用 ShopId、會員、GroupCode、TG 呼叫 CheckOrderOccupiedQuota。該方法以 PCPS get-by-label 的回傳筆數計算,查無資料回 0

DDB 主單狀態 預期 PCPS 佔額筆數
Occupy、WaitToReward 恰好 1 筆
Reward,點數尚未全回收;或券回饋 恰好 1 筆
Reward,點數已全回收 0 筆
Unmatch、MatchWithoutQuota、Cancel 0 筆

超出預期時,Auditor 會回報活動 ID、DDB 狀態、實際筆數與 groupId。這裡是計算 get-by-label 的查詢結果,**沒有在 Auditor 內逐筆驗證 Code 的 State**;查詢的 Limit 設為 30

同一批正向稽核中的其他檢查。 點數與券的紀錄 Auditor 都會指出「訂單已取消、失敗、出貨或完成,DDB 主單卻仍是 Occupy」;券還檢查 Occupy 時非 NoReward 子單是否為 WaitToReward。點數對線上佔額活動另查 DDB 回饋歷程是否出現過 Occupy,攤提舊版則跳過歷程檢查。PromotionRewardPerformanceAuditor 檢查應有的績效紀錄:WaitToReward、Reward 及曾經付款後才 Cancel 的紀錄須有績效;付款前 Occupy → Cancel 不要求績效

逆流程的返還檢查。 點數回收稽核在「線上訂單子單全數取消」時查 PCPS 是否仍有佔額,也會對 DDB Cancel 或 Reward 且點數全回收檢查是否仍有佔額;券回收稽核則對 DDB Cancel 檢查。這兩個 Auditor 都略過 Unmatch、MatchWithoutQuota 和沒有 GroupCode 的活動。若查到殘留佔額,會提示確認逆流程 Job 的返還是否完成。

排查一筆訂單時,可固定 ShopId + PromotionEngineId + TG + MemberId,依序核對活動 GroupCode、正向與逆流程 Job、DDB 狀態及歷程、PCPS 的 Code 筆數與 CodeId,再看績效 Job 是否應已建立。DDB 的 Occupy 代表系統記錄已佔額,實際是否仍占用額度仍須以 PCPS 查詢結果核對。

稽核判讀:CheckOrderOccupiedQuota 計算的是 PCPS get-by-label 回傳筆數,上限為 30;Auditor 本身沒有逐筆驗證 Code 的 State。