diff --git a/package.json b/package.json index 577c453..0c5c726 100644 --- a/package.json +++ b/package.json @@ -39,4 +39,4 @@ "npmextra.json", "readme.md" ] -} +} \ No newline at end of file diff --git a/readme.md b/readme.md index fa7adca..e07069d 100644 --- a/readme.md +++ b/readme.md @@ -27,7 +27,6 @@ Platform support | [![Supports Windows 10](https://badgen.net/badge/supports%20W Use TypeScript for best in class intellisense - ## Contribution We are always happy for code contributions. If you are not the code contributing type that is ok. Still, maintaining Open Source repositories takes considerable time and thought. If you like the quality of what we do and our modules are useful to you we would appreciate a little monthly contribution: You can [contribute one time](https://lossless.link/contribute-onetime) or [contribute monthly](https://lossless.link/contribute). :) diff --git a/test/test.ts b/test/test.ts index 087076f..adcc02f 100644 --- a/test/test.ts +++ b/test/test.ts @@ -11,6 +11,7 @@ tap.test('should create a valid paypal instance', async () => { testPayPalInstance = new paypal.PayPalAccount({ clientId: testQenv.getEnvVarOnDemand('PAYPAL_CLIENT_ID'), clientSecret: testQenv.getEnvVarOnDemand('PAYPAL_CLIENT_SECRET'), + accountOwner: 'sample corp' }); expect(testPayPalInstance).to.be.instanceOf(paypal.PayPalAccount); }); @@ -18,7 +19,7 @@ tap.test('should create a valid paypal instance', async () => { tap.test('should get an access token', async () => { const transactions = await testPayPalInstance.getTransactionsFromTo( smarttime.ExtendedDate.fromHyphedDate('2020-01-01').getTime(), - smarttime.ExtendedDate.fromHyphedDate('2020-08-01').getTime(), + smarttime.ExtendedDate.fromHyphedDate('2020-08-01').getTime() ); console.log(transactions); }); diff --git a/ts/paypal.classes.account.ts b/ts/paypal.classes.account.ts index 2e1659c..12a65f3 100644 --- a/ts/paypal.classes.account.ts +++ b/ts/paypal.classes.account.ts @@ -4,10 +4,11 @@ import { PayPalTransaction } from './paypal.classes.transaction'; export interface IPayPalOptions { clientId: string; clientSecret: string; + accountOwner: string; } export class PayPalAccount { - public apiBaseUrl: string = 'https://api.paypal.com' + public apiBaseUrl: string = 'https://api.paypal.com'; public options: IPayPalOptions; private apiToken: string; @@ -17,14 +18,104 @@ export class PayPalAccount { this.options = optionsArg; } - public async getTransactionsFromTo (fromTimeMillisArg: number, toTimeMillisArg: number) { - let returnTransactions: PayPalTransaction[] = []; + public async getTransactionsFromTo(fromTimeMillisArg: number, toTimeMillisArg: number) { + let allTransactions: PayPalTransaction[] = []; + // lets note the time from one month before. We need that for accurate transactions pools. + const monthBeforeStartMillis = + fromTimeMillisArg - plugins.smarttime.getMilliSecondsFromUnits({ days: 30 }); do { const transactions = await PayPalTransaction.getTransactionFor30days(this, fromTimeMillisArg); - returnTransactions = returnTransactions.concat(transactions); - fromTimeMillisArg = fromTimeMillisArg + plugins.smarttime.getMilliSecondsFromUnits({days: 30}); + allTransactions = allTransactions.concat(transactions); + fromTimeMillisArg = + fromTimeMillisArg + plugins.smarttime.getMilliSecondsFromUnits({ days: 30 }); } while (fromTimeMillisArg < toTimeMillisArg); - return returnTransactions; + + const invoiceIds: string[] = []; + const transactionPools: PayPalTransaction[][] = []; + // lets get all invoiceids + allTransactions.forEach((transactionArg) => { + const invoiceId = transactionArg.data.originApiObject.transaction_info.invoice_id; + if (!invoiceIds.includes(invoiceId)) { + invoiceIds.push(invoiceId); + } + }); + + // lets get all transactions per invoiceId + invoiceIds.forEach((invoiceIdArg) => { + const transactionPool: PayPalTransaction[] = []; + allTransactions.forEach((transactionArg) => { + if (transactionArg.data.originApiObject.transaction_info.invoice_id === invoiceIdArg) { + transactionPool.push(transactionArg); + } + }); + transactionPools.push(transactionPool); + }); + + const previousMonthTransactions = await PayPalTransaction.getTransactionFor30days( + this, + monthBeforeStartMillis + ); + + transactionPools.forEach((transactionPoolArg) => { + const poolInvoiceId = transactionPoolArg[0].data.originApiObject.transaction_info.invoice_id; + previousMonthTransactions.forEach((transactionArg) => { + if (transactionArg.data.originApiObject.transaction_info.invoice_id === poolInvoiceId) { + transactionPoolArg.push(transactionArg); + } + }); + }); + + let finalTransactions: PayPalTransaction[] = []; + // lets process all transactionPool + transactionPools.forEach((transactionPoolArg) => { + // lets detect foreign transactions + let hasForeignTransactions = false; + transactionPoolArg.forEach((transactionArg) => { + if (transactionArg.data.currency !== 'EUR') { + hasForeignTransactions = true; + } + }); + if (hasForeignTransactions && transactionPoolArg.length < 4) { + console.log( + `Pool with invoiceId ${transactionPoolArg[0].data.originApiObject.transaction_info.invoice_id} is not completed yet. Omminiting ${transactionPoolArg.length} transactions as a result.` + ); + return; + } + if (hasForeignTransactions && transactionPoolArg.length === 4) { + const negativeNativeTransaction = transactionPoolArg.find(transactionArg => { + return transactionArg.data.amount < 0 && transactionArg.data.currency === 'EUR'; + }); + + const negativeForeignTransaction = transactionPoolArg.find(transactionArg => { + return transactionArg.data.amount < 0 && transactionArg.data.currency !== 'EUR'; + }); + + const positiveNativeTransaction = transactionPoolArg.find(transactionArg => { + return transactionArg.data.amount > 0 && transactionArg.data.currency === 'EUR'; + }); + + const positiveForeignTransaction = transactionPoolArg.find(transactionArg => { + return transactionArg.data.amount > 0 && transactionArg.data.currency !== 'EUR'; + }); + + negativeNativeTransaction.data.name = negativeForeignTransaction.data.name; + negativeNativeTransaction.data.description = negativeForeignTransaction.data.description; + positiveNativeTransaction.data.name = this.options.accountOwner; + positiveNativeTransaction.data.description = 'account balance transfer'; + transactionPoolArg = transactionPoolArg.filter(transactionArg => transactionArg.data.currency === 'EUR'); + } + if (!hasForeignTransactions && transactionPoolArg.length === 2) { + const positiveNativeTransaction = transactionPoolArg.find(transactionArg => { + return transactionArg.data.amount > 0 && transactionArg.data.currency === 'EUR'; + }); + positiveNativeTransaction.data.name = this.options.accountOwner; + positiveNativeTransaction.data.description = 'account balance transfer'; + } + // pool is ready + finalTransactions = finalTransactions.concat(transactionPoolArg); + }); + + return finalTransactions; } public async request(methodArg: 'GET' | 'POST', routeArg: string, payloadArg: any) { diff --git a/ts/paypal.classes.transaction.ts b/ts/paypal.classes.transaction.ts index 02dd849..b825f2d 100644 --- a/ts/paypal.classes.transaction.ts +++ b/ts/paypal.classes.transaction.ts @@ -2,24 +2,58 @@ import * as plugins from './paypal.plugins'; import { PayPalAccount } from './paypal.classes.account'; export interface IPayPalOriginTransactionApiObject { - paypal_account_id: string; - transaction_id: string; - transaction_event_code: string; - transaction_initiation_date: string; - transaction_updated_date: string; - transaction_amount: { currency_code: string; value: string }; - transaction_status: string; - transaction_subject: string; - ending_balance: { currency_code: string; value: string }; - available_balance: { currency_code: string; value: string }; - invoice_id: string; - protection_eligibility: string; + transaction_info: { + paypal_account_id: string; + transaction_id: string; + transaction_event_code: string; + transaction_initiation_date: string; + transaction_updated_date: string; + transaction_amount: { currency_code: string; value: string }; + transaction_status: string; + transaction_subject: string; + ending_balance: { currency_code: string; value: string }; + available_balance: { currency_code: string; value: string }; + invoice_id: string; + protection_eligibility: string; + }; + cart_info: { + item_details: { + item_code: string; + item_name: string; + item_quantity: string; + item_unit_price: { + currency_code: string; + value: string; + }; + item_amount: { + currency_code: string; + value: string; + }; + total_item_amount: { + currency_code: string; + value: string; + }; + invoice_number: string; + }[]; + }; + payer_info: { + account_id: string; + email_address: string; + address_status: string; + payer_status: string; + payer_name: { + alternate_full_name: string; + }; + country_code: string; + }; } export interface IPayPalTransactionOptions { id: string; - originApiObject: IPayPalOriginTransactionApiObject + originApiObject: IPayPalOriginTransactionApiObject; amount: number; + name: string; + description: string; currency: 'USD' | 'EUR'; timestampIso: string; } @@ -36,19 +70,31 @@ export class PayPalTransaction { console.log(`getting PayPal transactions from ${startDateIso} + ${endDateIso}`); const response = await paypalInstanceArg.request( 'GET', - `/v1/reporting/transactions?start_date=${startDateIso}&end_date=${endDateIso}`, + `/v1/reporting/transactions?start_date=${startDateIso}&end_date=${endDateIso}&fields=all`, {} ); - const returnTransactions: PayPalTransaction[] = [] + const returnTransactions: PayPalTransaction[] = []; for (const transactionDetail of response.transaction_details) { - const apiObject: IPayPalOriginTransactionApiObject = transactionDetail.transaction_info; + const apiObject: IPayPalOriginTransactionApiObject = transactionDetail; const paypalTransaction = new PayPalTransaction({ originApiObject: apiObject, - id: apiObject.transaction_id, - amount: parseFloat(apiObject.transaction_amount.value), - currency: apiObject.transaction_amount.currency_code as 'EUR' | 'USD', - timestampIso: apiObject.transaction_initiation_date + id: apiObject.transaction_info.transaction_id, + amount: parseFloat(apiObject.transaction_info.transaction_amount.value), + currency: apiObject.transaction_info.transaction_amount.currency_code as 'EUR' | 'USD', + timestampIso: apiObject.transaction_info.transaction_initiation_date, + name: `${apiObject.payer_info.payer_name.alternate_full_name} (${apiObject.payer_info.email_address})`, + description: `${apiObject.cart_info?.item_details?.length} items: ${apiObject.cart_info?.item_details?.map(itemArg => { + return `${itemArg.item_name}`; + }).reduce((accumulatorArg, currentValue) => { + let returnString = ''; + if (accumulatorArg) { + returnString = accumulatorArg + ', ' + currentValue; + } else { + returnString = currentValue; + } + return returnString; + })}`, }); returnTransactions.push(paypalTransaction); }