// Node.js 24+. API key only in server environment. Do not log full responses. import {createInterface} from 'node:readline/promises' import {pathToFileURL} from 'node:url' import {setTimeout as delay} from 'node:timers/promises' const base=(process.env.TURBOPULSA_API_URL||'https://api.turbopulsa.com/sandbox/v1').replace(/\/$/,'') const parsed=new URL(base) if(!(parsed.protocol==='https:'&&parsed.hostname==='api.turbopulsa.com')&&!(process.env.TURBOPULSA_FIXTURE==='1'&&parsed.protocol==='http:'&&['127.0.0.1','localhost'].includes(parsed.hostname)))throw new Error('Use the branded HTTPS API; HTTP is fixture-only.') export async function api(path,method='GET',body=null,uuid=null){ const headers={Authorization:'Bearer '+process.env.TURBOPULSA_API_KEY,Accept:'application/json'} if(uuid){headers['Idempotency-Key']=uuid;headers['Content-Type']='application/json'} const r=await fetch(base+path,{method,headers,body:body?JSON.stringify(body):undefined,signal:AbortSignal.timeout(15000)}) const result=await r.json() if(!r.ok){const e=new Error(result.error?.code||'request_failed');e.status=r.status;e.result=result;throw e} return result } export const prepareQuote=(code,reference,destination,uuid)=>api('/quotes','POST',{product_code:code,client_reference:reference,destination},uuid) export async function confirmQuote(quote,uuid){ try{return await api('/orders/'+quote.id+'/confirm','POST',{confirmation_token:quote.confirmation_token},uuid)}catch(e){ if(e.result)throw e // Price change/expiry requires a new review, never silent reconfirmation. return await api('/orders/'+quote.id) // Ambiguous network outcome: check SAME order, never buy again. } } async function main(){ const reference=process.env.TURBOPULSA_CLIENT_REFERENCE,quoteUUID=process.env.TURBOPULSA_QUOTE_UUID,confirmUUID=process.env.TURBOPULSA_CONFIRM_UUID if(!reference||!quoteUUID||!confirmUUID)throw new Error('Persist client reference and both UUIDs before sending requests.') const postpaid=process.env.TURBOPULSA_KIND==='postpaid' const catalog=await api((postpaid?'/postpaid/catalog':'/catalog')+'?per_page=50'),code=process.env.TURBOPULSA_PRODUCT_CODE||catalog.data[0]?.code const destination=JSON.parse(process.env.TURBOPULSA_DESTINATION_JSON||'{"phone":"081200000000"}') let result=postpaid?await api('/postpaid/inquiries','POST',{product_code:code,client_reference:reference,customer_no:process.env.TURBOPULSA_CUSTOMER_NO||'001234567890'},quoteUUID):await prepareQuote(code,reference,destination,quoteUUID) for(let attempt=0;postpaid&&!result.data&&result.inquiry?.id&&attempt<5;attempt++){await delay(1000);result=await api('/postpaid/inquiries/'+result.inquiry.id)} const quote=result.data if(!quote?.id)throw new Error('Inquiry belum selesai. Periksa inquiry/referensi yang sama; jangan membuat pembelian baru.') console.log(JSON.stringify({id:quote.id,name:quote.name,price:quote.price,currency:quote.currency,status:quote.status})) const input=createInterface({input:process.stdin,output:process.stdout}) const approval=await input.question('Periksa tujuan lokal dan total di atas. Ketik BAYAR untuk konfirmasi: ');input.close() if(approval.trim()!=='BAYAR')return const {data:order}=await confirmQuote(quote,confirmUUID) const current=await api('/orders/'+order.id) console.log(JSON.stringify({id:current.data.id,status:current.data.status,charged:current.data.charged,currency:current.data.currency})) } if(process.argv[1]&&pathToFileURL(process.argv[1]).href===import.meta.url)main().catch(e=>{console.error(e.result?.error?.code||e.message);process.exitCode=1})