const crypto=require("crypto"),{Op}=require("sequelize"),db=require("../../models"),money=require("../pricing/money"),{nextSequence,pad}=require("../../utils/referenceNumber.util");const fault=(m,c,s=400)=>Object.assign(new Error(m),{code:c,status:s}); async function accountFor(userId,transaction){let account=await db.LoyaltyAccount.findOne({where:{user_id:userId},transaction,lock:transaction?.LOCK.UPDATE});if(!account)account=await db.LoyaltyAccount.create({id:crypto.randomUUID(),user_id:userId,status:"ACTIVE"},{transaction});if(account.status!=="ACTIVE")throw fault("Loyalty account is unavailable","LOYALTY_UNAVAILABLE",409);return account;} async function post({userId,eventId,type,sourceType,sourceId,points,expiresAt,descriptionCode,metadata},external){const run=async transaction=>{const existing=await db.LoyaltyLedgerEntry.findOne({where:{event_id:eventId},transaction,lock:transaction.LOCK.UPDATE});if(existing)return{entry:existing,idempotent:true};const account=await accountFor(userId,transaction);let available=account.available_points,debt=account.points_debt,spendable=0;if(points>0){const applied=Math.min(points,debt);debt-=applied;spendable=points-applied;available+=spendable;}else{const debit=-points;if(type==="REVERSAL"){const applied=Math.min(available,debit);available-=applied;debt+=debit-applied;}else if(available0)await db.LoyaltyPointAllocation.create({id:crypto.randomUUID(),earn_ledger_id:entry.id,loyalty_account_id:account.id,original_points:spendable,remaining_points:spendable,expires_at:expiresAt},{transaction});await evaluateTier(account,transaction);return{entry,idempotent:false};};return external?run(external):db.sequelize.transaction(run);} async function evaluateTier(account,transaction){const tier=await db.LoyaltyTier.findOne({where:{status:"ACTIVE",qualification_threshold:{[Op.lte]:account.lifetime_points_earned}},order:[["rank","DESC"]],transaction});if(tier&&tier.id!==account.current_tier_id){await db.LoyaltyTierHistory.create({id:crypto.randomUUID(),loyalty_account_id:account.id,from_tier_id:account.current_tier_id,to_tier_id:tier.id,reason:"LIFETIME_POINTS",effective_at:new Date()},{transaction});await account.update({current_tier_id:tier.id},{transaction});}return tier;} async function rule(sourceType,at,transaction){return db.LoyaltyEarnRule.findOne({where:{source_type:sourceType,status:"ACTIVE",effective_from:{[Op.lte]:at},[Op.or]:[{effective_to:null},{effective_to:{[Op.gt]:at}}]},order:[["effective_from","DESC"]],transaction});} async function earnPurchase(order,transaction){const r=await rule("PURCHASE",new Date(),transaction);if(!r||order.payment_status!=="PAID")return null;const eligible=money.subtractFloor(String(order.subtotal),String(order.discount_total)),units=money.parse(eligible)/money.parse(String(r.amount_unit)),raw=Number(units)*Number(r.points_per_amount||0),points=r.maximum_points?Math.min(raw,r.maximum_points):raw;if(!points||r.minimum_amount&&money.parse(eligible){const account=await accountFor(userId,transaction),prior=await db.LoyaltyRedemption.findOne({where:{loyalty_account_id:account.id,idempotency_key:idempotencyKey},transaction,lock:transaction.LOCK.UPDATE});if(prior)return{redemption:prior,idempotent:true};const reward=await db.LoyaltyReward.findOne({where:{id:rewardId,status:"ACTIVE",[Op.and]:[{[Op.or]:[{starts_at:null},{starts_at:{[Op.lte]:new Date()}}]},{[Op.or]:[{ends_at:null},{ends_at:{[Op.gt]:new Date()}}]}]},transaction,lock:transaction.LOCK.UPDATE});if(!reward)throw fault("Reward unavailable","REWARD_UNAVAILABLE",409);const count=await db.LoyaltyRedemption.count({where:{loyalty_account_id:account.id,reward_id:reward.id,status:"COMPLETED"},transaction}),total=await db.LoyaltyRedemption.count({where:{reward_id:reward.id,status:"COMPLETED"},transaction});if(reward.per_user_limit&&count>=reward.per_user_limit||reward.stock_limit&&total>=reward.stock_limit)throw fault("Reward limit reached","REWARD_LIMIT",409);if(account.available_points{const value=(money.parse(eligibleAmount)/money.parse(amountUnit))*BigInt(pointsPerAmount);return Number(maximumPoints?value>BigInt(maximumPoints)?BigInt(maximumPoints):value:value);};module.exports={accountFor,post,evaluateTier,earnPurchase,earnReview,reversePurchase,redeem,qualifyReferral,calculatePurchasePoints};