@@ -98,11 +98,23 @@ class PaymentListener {
9898 const block = await hiveClient . database . getBlock ( blockNum ) ;
9999 if ( ! block || ! block . transactions ) return ;
100100
101- for ( const tx of block . transactions ) {
101+ // transaction_ids array contains the IDs - tx.transaction_id is undefined
102+ const transactionIds = ( block as any ) . transaction_ids as string [ ] | undefined ;
103+
104+ for ( let txIndex = 0 ; txIndex < block . transactions . length ; txIndex ++ ) {
105+ const tx = block . transactions [ txIndex ] ;
106+ const trxId = transactionIds ?. [ txIndex ] ;
107+
108+ // Skip if no trx_id available
109+ if ( ! trxId ) {
110+ console . log ( `[PaymentListener] Missing trx_id for tx at index ${ txIndex } in block ${ blockNum } ` ) ;
111+ continue ;
112+ }
113+
102114 for ( const op of tx . operations ) {
103115 if ( op [ 0 ] === 'transfer' && op [ 1 ] . to === CONFIG . PAYMENT_ACCOUNT ) {
104116 await this . processTransfer ( {
105- trxId : tx . transaction_id ,
117+ trxId,
106118 blockNum,
107119 from : op [ 1 ] . from ,
108120 to : op [ 1 ] . to ,
@@ -155,43 +167,55 @@ class PaymentListener {
155167 return ;
156168 }
157169
158- // Calculate months based on amount - only credit what was actually paid
159- const monthsFromAmount = Math . floor ( amount / CONFIG . MONTHLY_PRICE_HBD ) ;
160-
161- if ( monthsFromAmount < 1 ) {
162- console . log ( '[PaymentListener] Insufficient amount:' , amount , 'HBD for' , parsed . username ) ;
163- await this . logPayment ( transfer , amount , 'failed' , 0 , null , 'Insufficient amount' ) ;
164- return ;
165- }
170+ // Process based on action
171+ if ( parsed . action === 'blog' ) {
172+ // For blog subscriptions: validate monthly payment
173+ const monthsFromAmount = Math . floor ( amount / CONFIG . MONTHLY_PRICE_HBD ) ;
166174
167- // If user requested more months than they paid for, reject
168- if ( parsed . months > monthsFromAmount ) {
169- console . log (
170- '[PaymentListener] Requested months exceed payment:' ,
171- parsed . months ,
172- 'requested but only' ,
173- monthsFromAmount ,
174- 'paid for' ,
175- parsed . username
176- ) ;
177- await this . logPayment (
178- transfer ,
179- amount ,
180- 'failed' ,
181- 0 ,
182- null ,
183- `Requested ${ parsed . months } months but only paid for ${ monthsFromAmount } `
184- ) ;
185- return ;
186- }
175+ if ( monthsFromAmount < 1 ) {
176+ console . log ( '[PaymentListener] Insufficient amount:' , amount , 'HBD for' , parsed . username ) ;
177+ await this . logPayment ( transfer , amount , 'failed' , 0 , null , 'Insufficient amount for subscription' ) ;
178+ return ;
179+ }
187180
188- // Grant only the months that were paid for
189- const months = monthsFromAmount ;
181+ // If user requested more months than they paid for, reject
182+ if ( parsed . months > monthsFromAmount ) {
183+ console . log (
184+ '[PaymentListener] Requested months exceed payment:' ,
185+ parsed . months ,
186+ 'requested but only' ,
187+ monthsFromAmount ,
188+ 'paid for' ,
189+ parsed . username
190+ ) ;
191+ await this . logPayment (
192+ transfer ,
193+ amount ,
194+ 'failed' ,
195+ 0 ,
196+ null ,
197+ `Requested ${ parsed . months } months but only paid for ${ monthsFromAmount } `
198+ ) ;
199+ return ;
200+ }
190201
191- // Process based on action
192- if ( parsed . action === 'blog' ) {
202+ // Grant only the months that were paid for
203+ const months = monthsFromAmount ;
193204 await this . processSubscription ( transfer , parsed . username , months , amount ) ;
194205 } else if ( parsed . action === 'upgrade' ) {
206+ // For upgrades: validate against pro upgrade price, not monthly price
207+ if ( amount < CONFIG . PRO_UPGRADE_PRICE_HBD ) {
208+ console . log ( '[PaymentListener] Insufficient amount for upgrade:' , amount , 'HBD' ) ;
209+ await this . logPayment (
210+ transfer ,
211+ amount ,
212+ 'failed' ,
213+ 0 ,
214+ null ,
215+ `Insufficient amount for Pro upgrade (need ${ CONFIG . PRO_UPGRADE_PRICE_HBD } HBD)`
216+ ) ;
217+ return ;
218+ }
195219 await this . processUpgrade ( transfer , parsed . username , amount ) ;
196220 }
197221 }
@@ -204,46 +228,74 @@ class PaymentListener {
204228 ) {
205229 console . log ( '[PaymentListener] Processing subscription:' , username , 'for' , months , 'months' ) ;
206230
207- try {
208- // Check if tenant exists, create if not
209- let tenant = await TenantService . getByUsername ( username ) ;
231+ let updatedTenant : any = null ;
210232
211- if ( ! tenant ) {
212- // Verify Hive account exists
213- const accountExists = await TenantService . verifyHiveAccount ( username ) ;
214- if ( ! accountExists ) {
215- console . log ( '[PaymentListener] Hive account not found:' , username ) ;
216- await this . logPayment ( transfer , amount , 'failed' , 0 , null , 'Hive account not found' ) ;
233+ try {
234+ // Use transaction to ensure atomicity of payment insert and subscription update
235+ await db . transaction ( async ( client ) => {
236+ // 1. Insert payment with 'processing' status first (serves as dedup via unique trx_id)
237+ const insertResult = await client . query (
238+ `INSERT INTO payments
239+ (tenant_id, trx_id, block_num, from_account, amount, currency, memo, status, months_credited)
240+ VALUES (NULL, $1, $2, $3, $4, 'HBD', $5, 'processing', $6)
241+ ON CONFLICT (trx_id) DO NOTHING
242+ RETURNING id` ,
243+ [ transfer . trxId , transfer . blockNum , transfer . from , amount , transfer . memo , months ]
244+ ) ;
245+
246+ // If no row returned, trx_id already exists (duplicate)
247+ if ( insertResult . rows . length === 0 ) {
248+ console . log ( '[PaymentListener] Duplicate transaction detected:' , transfer . trxId ) ;
217249 return ;
218250 }
219251
220- tenant = await TenantService . create ( username ) ;
221- console . log ( '[PaymentListener] Created new tenant:' , username ) ;
222- }
223-
224- // Activate/extend subscription
225- const updatedTenant = await TenantService . activateSubscription ( username , months ) ;
226-
227- // Generate config file
228- await ConfigService . generateConfigFile ( updatedTenant ) ;
229-
230- // Log successful payment
231- await this . logPayment (
232- transfer ,
233- amount ,
234- 'processed' ,
235- months ,
236- updatedTenant . subscription_expires_at
237- ) ;
252+ const paymentId = insertResult . rows [ 0 ] . id ;
253+
254+ // 2. Check if tenant exists, create if not
255+ let tenant = await TenantService . getByUsername ( username ) ;
256+
257+ if ( ! tenant ) {
258+ // Verify Hive account exists
259+ const accountExists = await TenantService . verifyHiveAccount ( username ) ;
260+ if ( ! accountExists ) {
261+ // Update payment to failed status
262+ await client . query (
263+ `UPDATE payments SET status = 'failed' WHERE id = $1` ,
264+ [ paymentId ]
265+ ) ;
266+ throw new Error ( 'Hive account not found' ) ;
267+ }
268+
269+ tenant = await TenantService . create ( username ) ;
270+ console . log ( '[PaymentListener] Created new tenant:' , username ) ;
271+ }
238272
239- console . log (
240- '[PaymentListener] Subscription activated for' ,
241- username ,
242- 'until' ,
243- updatedTenant . subscription_expires_at
244- ) ;
273+ // 3. Activate/extend subscription
274+ updatedTenant = await TenantService . activateSubscription ( username , months ) ;
275+
276+ // 4. Update payment to 'processed' with tenant_id and subscription info
277+ await client . query (
278+ `UPDATE payments
279+ SET tenant_id = $2, status = 'processed', subscription_extended_to = $3
280+ WHERE id = $1` ,
281+ [ paymentId , updatedTenant . id , updatedTenant . subscription_expires_at ]
282+ ) ;
283+
284+ console . log (
285+ '[PaymentListener] Subscription activated for' ,
286+ username ,
287+ 'until' ,
288+ updatedTenant . subscription_expires_at
289+ ) ;
290+ } ) ;
291+
292+ // 5. Generate config file AFTER transaction commits successfully
293+ if ( updatedTenant ) {
294+ await ConfigService . generateConfigFile ( updatedTenant ) ;
295+ }
245296 } catch ( error ) {
246297 console . error ( '[PaymentListener] Failed to process subscription for' , username , error ) ;
298+ // Log failure (may fail if trx_id already exists, which is fine)
247299 await this . logPayment ( transfer , amount , 'failed' , 0 , null , String ( error ) ) ;
248300 }
249301 }
0 commit comments