Edit: /tmp/tsx-0/17887-bf3f59a7c2d99a5c400393c12c6f1e918f1beba3 (30250B)
{"code":"__filename=\"/opt/canhelp/apps/api/src/routes/payments.ts\";(()=>{\n\"use strict\";var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __name=(target,value)=>__defProp(target,\"name\",{value,configurable:true});var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc2)=>{if(from&&typeof from===\"object\"||typeof from===\"function\"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc2=__getOwnPropDesc(from,key))||desc2.enumerable})}return to};var __toCommonJS=mod=>__copyProps(__defProp({},\"__esModule\",{value:true}),mod);var payments_exports={};__export(payments_exports,{default:()=>payments_default});module.exports=__toCommonJS(payments_exports);var import_hono=require(\"hono\");var import_crypto=require(\"crypto\");var import_drizzle_orm=require(\"drizzle-orm\");var import_db=require(\"../db.js\");var import_db2=require(\"@canhelp/db\");var import_auth=require(\"../middleware/auth.js\");var import_activity=require(\"../lib/activity.js\");var import_plan=require(\"../middleware/plan.js\");var import_email=require(\"../lib/email.js\");var import_balance=require(\"../lib/balance.js\");var import_plan_history=require(\"../lib/plan-history.js\");const app=new import_hono.Hono;const TIER_ORDER={free:0,pro:1,ultimate:2};async function getEPaySettings(){const rows=await import_db.db.select().from(import_db2.settings);const kv={};for(const r of rows)kv[r.key]=r.value;return{merchantId:kv[\"payment.epay.merchantId\"]??\"\",apiUser:kv[\"payment.epay.apiUser\"]??\"\",apiPassword:kv[\"payment.epay.apiPassword\"]??\"\",environment:kv[\"payment.epay.environment\"]??\"test\",confirmationUrl:kv[\"payment.epay.confirmationUrl\"]??\"\",cancelUrl:kv[\"payment.epay.cancelUrl\"]??\"\",enabled:kv[\"payment.epay.enabled\"]===\"true\"}}__name(getEPaySettings,\"getEPaySettings\");const EPAY_URL=\"https://paycenter.piraeusbank.gr/redirection/pay\";function computeInitDigest(tid,ref,amountCents,currency,password){const raw=`${tid};${ref};${amountCents};${currency};${password}`;return(0,import_crypto.createHash)(\"md5\").update(raw,\"utf8\").digest(\"base64\")}__name(computeInitDigest,\"computeInitDigest\");function computeConfirmDigest(ref,statusFlag,respCode,password){const raw=`${ref};${statusFlag};${respCode};${password}`;return(0,import_crypto.createHash)(\"md5\").update(raw,\"utf8\").digest(\"base64\")}__name(computeConfirmDigest,\"computeConfirmDigest\");app.post(\"/topup/init\",import_auth.requireAuth,async c=>{const user=c.get(\"user\");const body=await c.req.json().catch(()=>({amount:0}));const amount=Number(body.amount);if(!amount||amount<1||amount>9999){return c.json({error:\"Amount must be between 1 and 9999 EUR\"},400)}const cfg=await getEPaySettings();if(!cfg.enabled)return c.json({error:\"Payment gateway is disabled\"},503);if(!cfg.merchantId||!cfg.apiPassword){return c.json({error:\"Payment gateway is not configured\"},503)}const ts=Date.now();const rnd=Math.random().toString(36).slice(2,6).toUpperCase();const orderRef=`BAL-${ts}-${rnd}`;await import_db.db.insert(import_db2.payments).values({id:`pay_${ts}_${rnd}`,userId:user.id,orderRef,amount:amount.toFixed(2),currency:\"EUR\",type:\"balance\",status:\"pending\",description:`\\u041F\\u043E\\u043F\\u043E\\u043B\\u043D\\u0435\\u043D\\u0438\\u0435 \\u0431\\u0430\\u043B\\u0430\\u043D\\u0441\\u0430 \\u043D\\u0430 ${amount.toFixed(2)} EUR`});const amountCents=Math.round(amount*100);const currencyISO=\"978\";const digest=computeInitDigest(cfg.merchantId,orderRef,amountCents,currencyISO,cfg.apiPassword);const webUrl=process.env.WEB_URL||\"http://localhost:3000\";return c.json({url:EPAY_URL,params:{Tid:cfg.merchantId,MerchantReference:orderRef,ParamBackLink:`${webUrl}/payment?ref=${orderRef}`,AllowedPays:\"\",Quantity:\"1\",Amount:String(amountCents),Currency:currencyISO,Installments:\"0\",Iban:\"\",Encoding:\"UTF-8\",Lang:\"en\",Parameters:\"\",Digest:digest}})});app.post(\"/epay/confirm\",async c=>{let body={};try{const raw=await c.req.parseBody();for(const[k,v]of Object.entries(raw)){if(typeof v===\"string\")body[k]=v}}catch{return c.text(\"BAD_REQUEST\",400)}const{MerchantReference,StatusFlag,RespCode,TransactionId,Digest}=body;if(!MerchantReference||!StatusFlag||!RespCode){return c.text(\"MISSING_PARAMS\",400)}const cfg=await getEPaySettings();if(!cfg.apiPassword)return c.text(\"NOT_CONFIGURED\",503);const expected=computeConfirmDigest(MerchantReference,StatusFlag,RespCode,cfg.apiPassword);if(Digest!==expected){console.error(\"[ePay confirm] Invalid digest for order:\",MerchantReference);return c.text(\"INVALID_DIGEST\",400)}const[payment]=await import_db.db.select().from(import_db2.payments).where((0,import_drizzle_orm.eq)(import_db2.payments.orderRef,MerchantReference)).limit(1);if(!payment)return c.text(\"ORDER_NOT_FOUND\",404);if(payment.status!==\"pending\")return c.text(\"OK\");const success=StatusFlag===\"Success\"&&(RespCode===\"0x00\"||RespCode===\"00\");await import_db.db.transaction(async tx=>{await tx.update(import_db2.payments).set({status:success?\"success\":\"failed\",transactionId:TransactionId??null,ePayResponse:body,updatedAt:new Date}).where((0,import_drizzle_orm.eq)(import_db2.payments.orderRef,MerchantReference));if(success&&payment.type===\"balance\"){await(0,import_balance.applyBalanceChange)(tx,{userId:payment.userId,amount:Number(payment.amount),direction:\"credit\",kind:\"topup\",currency:payment.currency,paymentId:payment.id,description:payment.description,metadata:{orderRef:MerchantReference,transactionId:TransactionId??null}})}if(success&&payment.type===\"plan\"){const planIdMatch=(body.Parameters??payment.description??\"\").match(/planId=([^\\s&]+)/);const planId=planIdMatch?.[1];if(planId){const[plan]=await tx.select().from(import_db2.plans).where((0,import_drizzle_orm.eq)(import_db2.plans.id,planId));if(plan){const expiresAt=plan.durationDays?new Date(Date.now()+plan.durationDays*864e5):null;await tx.update(import_db2.users).set({planId:plan.id,planExpiresAt:expiresAt,updatedAt:new Date}).where((0,import_drizzle_orm.eq)(import_db2.users.id,payment.userId));(0,import_activity.logActivity)({userId:payment.userId,event:\"plan.purchase\",details:{planId:plan.id,planName:plan.name,method:\"epay\",price:payment.amount,orderRef:MerchantReference}}).catch(()=>{});(0,import_plan_history.logPlanHistory)({userId:payment.userId,event:\"activated\",planId:plan.id,planName:plan.name,expiresAt})}}}});if(success&&payment.type===\"balance\"){(0,import_activity.logActivity)({userId:payment.userId,event:\"payment.topup\",details:{amount:payment.amount,currency:payment.currency,orderRef:MerchantReference,transactionId:TransactionId}}).catch(()=>{})}return c.text(\"OK\")});app.get(\"/epay/cancel\",async c=>{const ref=c.req.query(\"MerchantReference\")||c.req.query(\"ref\");if(ref){await import_db.db.update(import_db2.payments).set({status:\"cancelled\",updatedAt:new Date}).where((0,import_drizzle_orm.eq)(import_db2.payments.orderRef,ref)).catch(()=>{})}const webUrl=process.env.WEB_URL||\"http://localhost:3000\";return c.redirect(`${webUrl}/payment?status=cancelled`)});app.get(\"/history\",import_auth.requireAuth,async c=>{const user=c.get(\"user\");const ref=(c.req.query(\"ref\")||\"\").trim();const whereExpr=ref?(0,import_drizzle_orm.and)((0,import_drizzle_orm.eq)(import_db2.payments.userId,user.id),(0,import_drizzle_orm.eq)(import_db2.payments.orderRef,ref)):(0,import_drizzle_orm.eq)(import_db2.payments.userId,user.id);const rows=await import_db.db.select().from(import_db2.payments).where(whereExpr).orderBy((0,import_drizzle_orm.desc)(import_db2.payments.createdAt)).limit(ref?1:50);return c.json(rows)});app.get(\"/balance-history\",import_auth.requireAuth,async c=>{const user=c.get(\"user\");const limit=Math.min(Number(c.req.query(\"limit\")||50),100);const rows=await(0,import_balance.getUserBalanceHistory)(import_db.db,user.id,limit);return c.json(rows)});app.post(\"/plan/preview\",import_auth.requireAuth,async c=>{const user=c.get(\"user\");const body=await c.req.json().catch(()=>({planId:\"\"}));if(!body.planId)return c.json({error:\"planId is required\"},400);const[targetPlan]=await import_db.db.select().from(import_db2.plans).where((0,import_drizzle_orm.eq)(import_db2.plans.id,body.planId));if(!targetPlan||!targetPlan.isActive)return c.json({error:\"Plan not found\"},404);const[dbUser]=await import_db.db.select({planId:import_db2.users.planId,planExpiresAt:import_db2.users.planExpiresAt,balance:import_db2.users.balance}).from(import_db2.users).where((0,import_drizzle_orm.eq)(import_db2.users.id,user.id));const currentBalance=Number(dbUser?.balance??0);const targetPrice=Number(targetPlan.price);if(targetPrice===0){return c.json({type:\"free\",effectivePrice:\"0.00\",prorated:\"0.00\",currentBalance:currentBalance.toFixed(2),canPayFromBalance:true,scheduledDate:null})}const hasActivePlan=dbUser?.planId&&dbUser?.planExpiresAt&&dbUser.planExpiresAt>new Date;if(!hasActivePlan){return c.json({type:\"upgrade\",effectivePrice:targetPrice.toFixed(2),prorated:\"0.00\",currentBalance:currentBalance.toFixed(2),canPayFromBalance:currentBalance>=targetPrice,scheduledDate:null})}const[currentPlan]=await import_db.db.select().from(import_db2.plans).where((0,import_drizzle_orm.eq)(import_db2.plans.id,dbUser.planId));if(!currentPlan){return c.json({type:\"upgrade\",effectivePrice:targetPrice.toFixed(2),prorated:\"0.00\",currentBalance:currentBalance.toFixed(2),canPayFromBalance:currentBalance>=targetPrice,scheduledDate:null})}const currentTier=TIER_ORDER[currentPlan.tier]??0;const targetTier=TIER_ORDER[targetPlan.tier]??0;if(currentTier===targetTier){return c.json({type:\"same\",effectivePrice:targetPrice.toFixed(2),prorated:\"0.00\",currentBalance:currentBalance.toFixed(2),canPayFromBalance:currentBalance>=targetPrice,scheduledDate:null})}if(targetTier>currentTier){const expiresAt=dbUser.planExpiresAt;const totalMs=(currentPlan.durationDays??30)*864e5;const remainingMs=Math.max(0,expiresAt.getTime()-Date.now());const prorated=Number(currentPlan.price)*(remainingMs/totalMs);const effectivePrice=Math.max(0,targetPrice-prorated);return c.json({type:\"upgrade\",effectivePrice:effectivePrice.toFixed(2),prorated:prorated.toFixed(2),currentBalance:currentBalance.toFixed(2),canPayFromBalance:currentBalance>=effectivePrice,scheduledDate:null})}return c.json({type:\"downgrade\",effectivePrice:\"0.00\",prorated:\"0.00\",currentBalance:currentBalance.toFixed(2),canPayFromBalance:true,scheduledDate:dbUser.planExpiresAt.toISOString()})});app.post(\"/plan/purchase\",import_auth.requireAuth,async c=>{const user=c.get(\"user\");const body=await c.req.json().catch(()=>({planId:\"\",payFromBalance:false}));if(!body.planId)return c.json({error:\"planId is required\"},400);const[targetPlan]=await import_db.db.select().from(import_db2.plans).where((0,import_drizzle_orm.eq)(import_db2.plans.id,body.planId));if(!targetPlan||!targetPlan.isActive)return c.json({error:\"Plan not found or inactive\"},404);const[dbUser]=await import_db.db.select({planId:import_db2.users.planId,planExpiresAt:import_db2.users.planExpiresAt,balance:import_db2.users.balance,name:import_db2.users.name,email:import_db2.users.email,locale:import_db2.users.locale}).from(import_db2.users).where((0,import_drizzle_orm.eq)(import_db2.users.id,user.id));const currentBalance=Number(dbUser?.balance??0);const targetPrice=Number(targetPlan.price);const userLocale=dbUser?.locale??\"el\";if(targetPrice===0){await import_db.db.update(import_db2.users).set({planId:targetPlan.id,planExpiresAt:null,pendingPlanId:null,updatedAt:new Date}).where((0,import_drizzle_orm.eq)(import_db2.users.id,user.id));(0,import_email.emailPlanActivated)({to:dbUser.email,name:dbUser?.name??\"\",planName:targetPlan.name,price:\"0.00\",expiresAt:\"\\u2014\",locale:userLocale}).catch(()=>{});(0,import_activity.logActivity)({userId:user.id,event:\"plan.change\",details:{planId:targetPlan.id,planName:targetPlan.name,method:\"free\"}}).catch(()=>{});(0,import_plan_history.logPlanHistory)({userId:user.id,event:\"activated\",planId:targetPlan.id,planName:targetPlan.name,previousPlanId:dbUser?.planId??null,expiresAt:null});(0,import_plan.invalidatePlanCache)(user.id);return c.json({success:true,plan:targetPlan,method:\"free\"})}const hasActivePlan=dbUser?.planId&&dbUser?.planExpiresAt&&dbUser.planExpiresAt>new Date;let currentPlan=null;if(hasActivePlan&&dbUser?.planId){const[cp]=await import_db.db.select().from(import_db2.plans).where((0,import_drizzle_orm.eq)(import_db2.plans.id,dbUser.planId));currentPlan=cp??null}const currentTier=TIER_ORDER[currentPlan?.tier??\"free\"]??0;const targetTier=TIER_ORDER[targetPlan.tier]??0;if(currentPlan&&targetTier
{});(0,import_activity.logActivity)({userId:user.id,event:\"plan.downgrade_scheduled\",details:{planId:targetPlan.id,planName:targetPlan.name,scheduledDate:dbUser.planExpiresAt}}).catch(()=>{});return c.json({success:true,scheduled:true,activeUntil:dbUser.planExpiresAt,plan:targetPlan})}let effectivePrice=targetPrice;let prorated=0;if(currentPlan&&targetTier>currentTier&&hasActivePlan&&dbUser?.planExpiresAt){const totalMs=(currentPlan.durationDays??30)*864e5;const remainingMs=Math.max(0,dbUser.planExpiresAt.getTime()-Date.now());prorated=Number(currentPlan.price)*(remainingMs/totalMs);effectivePrice=Math.max(0,targetPrice-prorated)}if(body.payFromBalance){if(currentBalance{const[payment]=await tx.insert(import_db2.payments).values({id:`pay_${ts2}_${rnd2}`,userId:user.id,orderRef:`PLAN-${ts2}-${rnd2}`,amount:effectivePrice.toFixed(2),currency:targetPlan.currency,type:\"plan\",status:\"success\",description:`Plan: ${targetPlan.name}`}).returning();await(0,import_balance.applyBalanceChange)(tx,{userId:user.id,amount:effectivePrice,direction:\"debit\",kind:\"plan_purchase\",currency:targetPlan.currency,paymentId:payment.id,description:`Plan: ${targetPlan.name}`,metadata:{planId:targetPlan.id,planName:targetPlan.name,prorated:prorated.toFixed(2)}});await tx.update(import_db2.users).set({planId:targetPlan.id,planExpiresAt:expiresAt,pendingPlanId:null,renewalReminderSentAt:null,updatedAt:new Date}).where((0,import_drizzle_orm.eq)(import_db2.users.id,user.id))});const expiresStr=expiresAt?.toLocaleDateString(\"el-GR\")??\"\\u2014\";if(currentPlan&&targetTier>currentTier){(0,import_email.emailPlanUpgraded)({to:dbUser.email,name:dbUser?.name??\"\",oldPlanName:currentPlan.name,newPlanName:targetPlan.name,charged:effectivePrice.toFixed(2),expiresAt:expiresStr,locale:userLocale}).catch(()=>{})}else{(0,import_email.emailPlanActivated)({to:dbUser.email,name:dbUser?.name??\"\",planName:targetPlan.name,price:effectivePrice.toFixed(2),expiresAt:expiresStr,locale:userLocale}).catch(()=>{})}(0,import_plan.invalidatePlanCache)(user.id);(0,import_activity.logActivity)({userId:user.id,event:\"plan.purchase\",details:{planId:targetPlan.id,planName:targetPlan.name,method:\"balance\",effectivePrice:effectivePrice.toFixed(2),prorated:prorated.toFixed(2)}}).catch(()=>{});const historyEvent=currentPlan&&targetTier>currentTier?\"activated\":currentPlan&&targetTier