feat: implement membership validation logic to distinguish between manual and auto-tier loyalty programs in POS orders

This commit is contained in:
Suherdy Yacob 2026-06-04 10:37:26 +07:00
parent 9d1fe545e3
commit 1f85ab68bc

View File

@ -37,6 +37,37 @@ patch(PosOrder.prototype, {
// Custom Logic for Multi-Level Membership.
// Guard against recursive calls triggered when filtering all programs below.
if (program.multi_level_membership && !_checkingMultiLevel) {
const partner = this.getPartner();
const membershipId = (partner && partner.membership_level_id)
? resolveManyToOneId(partner.membership_level_id)
: null;
// ── Case 1: Manual membership program (e.g. Membership Direksi, subscriptions) ──
// These programs are never part of auto-tier selection.
// Only allow them when the partner actually holds an active loyalty card
// for this program — whether it's a replacement tier (Direksi) or
// an add-on subscription (e.g. Makan Pagi Gratis).
if (program.manual_membership) {
if (!partner) return false;
const allCards = this.models['loyalty.card']?.getAll() || [];
return allCards.some((card) => {
const cardPartnerId = resolveManyToOneId(card.partner_id);
const cardProgramId = resolveManyToOneId(card.program_id);
return cardPartnerId === partner.id && cardProgramId === program.id;
});
}
// ── Case 2: Auto-tier program (Silver / Gold / Platinum) ──
// If the partner holds a manual membership level (e.g. Direksi),
// block all auto-tier rewards — they use their own program's rewards.
if (membershipId) {
const allPrograms = this.models['loyalty.program'].getAll();
const memberProgram = allPrograms.find((p) => p.id === membershipId);
if (memberProgram && memberProgram.manual_membership) {
return false;
}
}
// Retrieve all loyalty programs
const allPrograms = this.models['loyalty.program'].getAll();
@ -44,9 +75,10 @@ patch(PosOrder.prototype, {
_checkingMultiLevel = true;
let multiLevelPrograms;
try {
// Filter programs that have the multi-level flag and pass the base applicability check.
// With the guard set, this._programIsApplicable(p) will skip the custom block,
// effectively calling only the base logic for each candidate program.
// Filter programs that have the multi-level flag (non-manual) and
// pass the base applicability check. With the guard set,
// _programIsApplicable(p) will skip this custom block,
// effectively calling only the base logic for each candidate.
multiLevelPrograms = allPrograms.filter(
(p) => p.multi_level_membership && !p.manual_membership && this._programIsApplicable(p)
);
@ -54,24 +86,19 @@ patch(PosOrder.prototype, {
_checkingMultiLevel = false;
}
// If there are no applicable multi-level programs, block all of them.
// If there are no applicable auto-tier programs, block all of them.
if (multiLevelPrograms.length === 0) {
return false;
}
let bestProgram = null;
const partner = this.getPartner();
// If the partner is set and has a membership level, try to match it.
if (partner && partner.membership_level_id) {
const membershipId = resolveManyToOneId(partner.membership_level_id);
// If the partner has a membership level, try to match it.
if (membershipId) {
// Find the matching program among applicable multi-level programs
bestProgram = multiLevelPrograms.find((p) => p.id === membershipId) || null;
}
}
// Fallback: pick the program with the smallest minimum_spend
// Fallback: pick the program with the smallest minimum_spend (Silver)
if (!bestProgram) {
bestProgram = multiLevelPrograms.reduce((prev, curr) => {
const prevSpend = prev.minimum_spend || 0;
@ -89,3 +116,4 @@ patch(PosOrder.prototype, {
return true;
},
});