-
Notifications
You must be signed in to change notification settings - Fork 4
(Online shop) u2u payment processing, change to “3-wallet with queuing” approach #283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
9f7986f
e942963
5732313
d6daa8a
9750702
9fad5e2
7ba8597
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import logger from "../../config/loggingConfig"; | ||
| import A2UPaymentQueue from "../../models/A2UPaymentQueue"; | ||
| import { createA2UPayment } from "../../services/payment.service"; | ||
|
|
||
| // workers/mongodbA2UWorker.ts | ||
| async function processNextJob(): Promise<void> { | ||
| const now = new Date(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Declared but not used. |
||
| const MAXATTEMPT = 3 | ||
|
|
||
| const threeDaysAgo = new Date(); | ||
| threeDaysAgo.setDate(threeDaysAgo.getDate() - 3); | ||
|
|
||
| const job = await A2UPaymentQueue.findOneAndUpdate( | ||
| { | ||
| $or: [ | ||
| { status: 'pending' }, | ||
| { status: 'failed' }, | ||
| { | ||
| status: 'batching', | ||
| last_a2u_date: { $lte: threeDaysAgo } | ||
| } | ||
| ], | ||
| attempts: { $lt: 3 } | ||
| }, | ||
| { | ||
| status: 'processing', | ||
| $inc: { attempts: 1 }, | ||
| updatedAt: new Date(), | ||
| }, | ||
| { | ||
| sort: { updatedAt: 1 }, | ||
| new: true, | ||
| } | ||
| ); | ||
|
|
||
|
|
||
| if (!job) return; | ||
|
|
||
| const { sellerPiUid, amount, xRef_ids, _id, attempts, memo, last_a2u_date } = job; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Though it can be considered in the enqueue function, it’s is not used because of the payment batching for gas saver, that is multiple payment from different buyers with different memo descriptions. |
||
|
|
||
| try { | ||
| logger.info(`[→] Attempt ${attempts}/${MAXATTEMPT} for ${sellerPiUid}`); | ||
|
|
||
| await createA2UPayment({ | ||
| sellerPiUid: sellerPiUid, | ||
| amount: amount.toString(), | ||
| memo: "A2U payment", | ||
| xRefIds: xRef_ids | ||
| }) | ||
|
|
||
| await A2UPaymentQueue.findByIdAndUpdate(_id, { | ||
| status: 'completed', | ||
| updatedAt: new Date(), | ||
| last_a2u_date: new Date(), | ||
| last_error: null, | ||
| }); | ||
|
|
||
| console.log(`[✔] A2U payment completed for ${sellerPiUid}`); | ||
| } catch (err: any) { | ||
|
|
||
| const willRetry = attempts < MAXATTEMPT; | ||
|
|
||
| await A2UPaymentQueue.findByIdAndUpdate(_id, { | ||
| status: willRetry ? 'pending' : 'failed', | ||
| last_error: err.message, | ||
| updatedAt: new Date(), | ||
| }); | ||
|
|
||
| logger.error(`[✘] A2U payment failed for ${sellerPiUid}: ${err.message}`); | ||
| if (!willRetry) { | ||
| logger.info(`[⚠️] Job permanently failed after ${attempts} attempts.`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export default processNextJob; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,67 +1,67 @@ | ||
| import Seller from "../../models/Seller"; | ||
| import { SanctionedSellerStatus } from "../../types"; | ||
| import { getAllSanctionedRegions } from "../../services/admin/report.service"; | ||
| import { | ||
| createBulkPreRestrictionOperation, | ||
| createGeoQueries | ||
| } from "../utils/geoUtils"; | ||
| import { | ||
| getSellersToEvaluate, | ||
| processSellersGeocoding, | ||
| processSanctionedSellers, | ||
| processUnsanctionedSellers | ||
| } from "../utils/sanctionUtils"; | ||
| import logger from "../../config/loggingConfig"; | ||
| // import Seller from "../../models/Seller"; | ||
| // import { SanctionedSellerStatus } from "../../types"; | ||
| // import { getAllSanctionedRegions } from "../../services/admin/report.service"; | ||
| // import { | ||
| // createBulkPreRestrictionOperation, | ||
| // createGeoQueries | ||
| // } from "../utils/geoUtils"; | ||
| // import { | ||
| // getSellersToEvaluate, | ||
| // processSellersGeocoding, | ||
| // processSanctionedSellers, | ||
| // processUnsanctionedSellers | ||
| // } from "../utils/sanctionUtils"; | ||
| // import logger from "../../config/loggingConfig"; | ||
|
|
||
| export async function runSanctionBot(): Promise<void> { | ||
| logger.info('Sanction Bot cron job started.'); | ||
| // export async function runSanctionBot(): Promise<void> { | ||
| // logger.info('Sanction Bot cron job started.'); | ||
|
|
||
| try { | ||
| /* Step 1: Reset the 'isPreRestricted' field to 'false' for all sellers | ||
| This clears any pre-existing restrictions before applying new ones. */ | ||
| await Seller.updateMany({}, { isPreRestricted: false }).exec(); | ||
| logger.info('Reset [isPreRestricted] for all sellers.'); | ||
| // try { | ||
| // /* Step 1: Reset the 'isPreRestricted' field to 'false' for all sellers | ||
| // This clears any pre-existing restrictions before applying new ones. */ | ||
| // await Seller.updateMany({}, { isPreRestricted: false }).exec(); | ||
| // logger.info('Reset [isPreRestricted] for all sellers.'); | ||
|
|
||
| /* Step 2: Get the list of all sanctioned regions */ | ||
| const sanctionedRegions = await getAllSanctionedRegions(); | ||
| // If no sanctioned regions are found, log the info and exit the job | ||
| if (!sanctionedRegions.length) { | ||
| logger.info('No sanctioned regions found. Exiting job.'); | ||
| return; | ||
| } | ||
| // /* Step 2: Get the list of all sanctioned regions */ | ||
| // const sanctionedRegions = await getAllSanctionedRegions(); | ||
| // // If no sanctioned regions are found, log the info and exit the job | ||
| // if (!sanctionedRegions.length) { | ||
| // logger.info('No sanctioned regions found. Exiting job.'); | ||
| // return; | ||
| // } | ||
|
|
||
| /* Step 3: Create geo-based queries and identify sellers to evaluate */ | ||
| const geoQueries = createGeoQueries(sanctionedRegions); | ||
| const sellersToEvaluate = await getSellersToEvaluate(geoQueries); | ||
| logger.info(`Evaluating ${sellersToEvaluate.length} sellers flagged or currently Restricted.`); | ||
| // /* Step 3: Create geo-based queries and identify sellers to evaluate */ | ||
| // const geoQueries = createGeoQueries(sanctionedRegions); | ||
| // const sellersToEvaluate = await getSellersToEvaluate(geoQueries); | ||
| // logger.info(`Evaluating ${sellersToEvaluate.length} sellers flagged or currently Restricted.`); | ||
|
|
||
| /* Step 4: Create the bulk update operations to mark sellers as pre-restricted */ | ||
| const bulkPreRestrictionOps = createBulkPreRestrictionOperation(sellersToEvaluate); | ||
| if (bulkPreRestrictionOps.length > 0) { | ||
| await Seller.bulkWrite(bulkPreRestrictionOps); | ||
| logger.info(`Marked ${bulkPreRestrictionOps.length} sellers as Pre-Restricted`) | ||
| } | ||
| // /* Step 4: Create the bulk update operations to mark sellers as pre-restricted */ | ||
| // const bulkPreRestrictionOps = createBulkPreRestrictionOperation(sellersToEvaluate); | ||
| // if (bulkPreRestrictionOps.length > 0) { | ||
| // await Seller.bulkWrite(bulkPreRestrictionOps); | ||
| // logger.info(`Marked ${bulkPreRestrictionOps.length} sellers as Pre-Restricted`) | ||
| // } | ||
|
|
||
| /* Step 5: Retrieve all sellers who are marked as pre-restricted */ | ||
| const preRestrictedSellers = await Seller.find({isPreRestricted: true}).exec(); | ||
| logger.info(`${preRestrictedSellers.length} sellers are Pre-Restricted`); | ||
| // /* Step 5: Retrieve all sellers who are marked as pre-restricted */ | ||
| // const preRestrictedSellers = await Seller.find({isPreRestricted: true}).exec(); | ||
| // logger.info(`${preRestrictedSellers.length} sellers are Pre-Restricted`); | ||
|
|
||
| /* Step 6: Process geocoding validation */ | ||
| const results: SanctionedSellerStatus[] = await processSellersGeocoding( | ||
| preRestrictedSellers, | ||
| sanctionedRegions | ||
| ); | ||
| const inZone = results.filter(r => r.isSanctionedRegion); | ||
| const outOfZone = results.filter(r => !r.isSanctionedRegion); | ||
| // /* Step 6: Process geocoding validation */ | ||
| // const results: SanctionedSellerStatus[] = await processSellersGeocoding( | ||
| // preRestrictedSellers, | ||
| // sanctionedRegions | ||
| // ); | ||
| // const inZone = results.filter(r => r.isSanctionedRegion); | ||
| // const outOfZone = results.filter(r => !r.isSanctionedRegion); | ||
|
|
||
| /* Step 7: Apply restrictions of in-zone sellers or restoration of out-zone sellers */ | ||
| await processSanctionedSellers(inZone); | ||
| await processUnsanctionedSellers(outOfZone); | ||
| // /* Step 7: Apply restrictions of in-zone sellers or restoration of out-zone sellers */ | ||
| // await processSanctionedSellers(inZone); | ||
| // await processUnsanctionedSellers(outOfZone); | ||
|
|
||
| /* Step 8: Clean up temp pre-restriction flags */ | ||
| await Seller.updateMany({isPreRestricted: true}, {isPreRestricted: false}).exec(); | ||
| logger.info('SanctionBot job completed.'); | ||
| } catch (error) { | ||
| logger.error('Error in Sanction Bot cron job:', error); | ||
| } | ||
| } | ||
| // /* Step 8: Clean up temp pre-restriction flags */ | ||
| // await Seller.updateMany({isPreRestricted: true}, {isPreRestricted: false}).exec(); | ||
| // logger.info('SanctionBot job completed.'); | ||
| // } catch (error) { | ||
| // logger.error('Error in Sanction Bot cron job:', error); | ||
| // } | ||
| // } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import mongoose, { Schema } from "mongoose"; | ||
| import { IA2UJob } from "../types"; | ||
|
|
||
| const A2UPaymentQueueSchema = new Schema<IA2UJob>( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @adisa39 - I noticed that this DB schema doesn’t align with the PaymentQueue model outlined in the schema documentation @ https://docs.google.com/document/d/1i9JjD3veU4RmZXEiD_D9j7XvRIcZm9koJ_DFDITe1H0/edit?usp=sharing It would have been helpful to get your feedback during the schema design phase or possibly during implementation. Just checking — was there a reason you disagreed with the proposed model, or was this an oversight? Thanks!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @swoocn - please note that various approaches have been tried before arriving at this implementation. So my thought is to raised this once execution is done and solid. The proposed model has some repeated fields that can be accessed from other collections using foreign keys. More importantly batching of payments from different buyers with different payment details (payer_pi_username, u2a_blockchain_id, description etc.) cannot go with this feature implementation. Please let me know your thoughts on this.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Understood; no problem @adisa39! |
||
| { | ||
| sellerPiUid: { type: String, required: true }, | ||
| amount: { type: Number, required: true }, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't amount field be Types.Decimal128 to maintain data type consistency?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This type usually difficult to handle in the BE/FE logics
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gotcha! I can see how that may be the case. Let's stick to just Number for now then. |
||
| xRef_ids: [{ type: String, required: true }], | ||
| memo: { type: String, require: true }, | ||
| status: { | ||
| type: String, | ||
| enum: ['pending', 'processing', 'completed', 'failed', 'batching'], | ||
| default: 'pending', | ||
| }, | ||
| last_a2u_date: { type: Date, default: null }, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @adisa39 - could you clarify the difference between the last_a2u_date timestamp and the updatedAt timestamp in the context of A2U queue payment processing? Thanks, in advance!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The last_a2u_date is used in the payment batching to determine the date of the next A2U payment for sellers with their gas saver turned on (3days from last payout). While the updatedAt keeps tracks of payment status changes while determining its position on the queue.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then perhaps it would make sense to rename the field to something like last_batched_a2u_date or last-gas-saver-a2u-date? Thoughts?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not necessary, since it’s applicable to all records and sellers with their gas saver turned off can also turned it on
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about last_a2u_payout_date @adisa39? |
||
| attempts: { type: Number, default: 0 }, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we change the attempts field to num_retries as per the DB schema document?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes |
||
| last_error: { type: String, default: null } | ||
| }, | ||
| { timestamps: true } | ||
| ); | ||
|
|
||
| const A2UPaymentQueue = mongoose.model<IA2UJob>('A2UPaymentQueue', A2UPaymentQueueSchema); | ||
| export default A2UPaymentQueue; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ll work on tidying up the cron job setup so both your A2U process and the Sanction bot can run in parallel.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh! That will be nice, thanks.
I don’t know we are still using the cron job for sanction regions implementation, they sections were all commented out before I started working on it.