Skip to content
This repository has been archived by the owner on Jan 7, 2025. It is now read-only.

Commit

Permalink
refund balance checks
Browse files Browse the repository at this point in the history
  • Loading branch information
harshasomisetty committed Aug 28, 2023
1 parent 7d5fb00 commit 63da4ea
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 101 deletions.
81 changes: 3 additions & 78 deletions apps/backend-serverless/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ function generateMerchantRecords(count = 1): any[] {
acceptedPrivacyPolicy: true,
dismissCompleted: true,
active: true,
loyaltyProgram: 'tiers',
pointsMint: 'Fq2oteAH3w4qKfDtnrdHTqVNRoUAWwMHSqeG7gsRqPSC',
pointsBack: 1,
// loyaltyProgram: 'tiers',
// pointsMint: 'Fq2oteAH3w4qKfDtnrdHTqVNRoUAWwMHSqeG7gsRqPSC',
// pointsBack: 1,
};
} else {
merchant = {
Expand All @@ -80,65 +80,6 @@ function generateMerchantRecords(count = 1): any[] {
return records;
}

function generatePaymentRecords(count = 1): any[] {
const records: any[] = [];
for (let i = 0; i < count; i++) {
const requestedAt = new Date();
requestedAt.setDate(requestedAt.getDate() + i);
const completedAt = new Date(requestedAt.getTime());
completedAt.setDate(completedAt.getDate() + i + 1);

const record = {
id: `payment-${i}`,
status: 'completed',
shopId: `r_2-${i}_shopid`,
shopGid: `gid://shopify/PaymentSession/r_${i}_shopid`,
shopGroup: `shop_group_${i}`,
test: 1,
amount: i + 1,
currency: 'USD',
usdcAmount: i + 1,
cancelURL: `https://store${i}.myshopify.com/checkouts/c/randomId_-${i}/processing`,
merchantId: `GZQN3FYe8WGLTWSBGDgprSfJmwwDrYNPL2vR2v9ZpJof`,
transactionSignature: `317CdVpw26TCBpgKdaK8siAG3iMHatFPxph47GQieaZYojo9Q4qNG8vJ3r2EsHUWGEieEgzpFYBPmrqhiHh6sjLt`,
requestedAt: requestedAt.toISOString(),
completedAt: completedAt.toISOString(),
};

records.push(record);
}
return records;
}

function generateRefundRecords(paymentRecords: any[]): any[] {
const records: any[] = [];

for (let i = 0; i < paymentRecords.length; i++) {
const paymentRecord = paymentRecords[i];
const requestedAt = new Date(paymentRecord.requestedAt);
const completedAt = new Date(paymentRecord.completedAt);

const record = {
id: `refund-${i}`,
status: i % 2 === 0 ? 'pending' : 'completed',
amount: i,
currency: 'USD',
usdcAmount: i,
shopId: `r_${i}_shopid`,
shopGid: `gid://shopify/PaymentSession/r_${i}_shopid`,
shopPaymentId: paymentRecord.shopId,
test: 1,
merchantId: paymentRecord.merchantId,
transactionSignature: i % 2 === 0 ? null : `signature-${i}`,
requestedAt: requestedAt.toISOString(),
completedAt: i % 2 === 0 ? null : completedAt.toISOString(),
};

records.push(record);
}
return records;
}

function generateProductRecords(count = 2): any[] {
const records: any[] = [
{
Expand Down Expand Up @@ -199,22 +140,6 @@ async function insertGeneratedData(merchants: number, payments: number, products
})),
});

const paymentRecords = await prisma.paymentRecord.createMany({
data: generatePaymentRecords(payments).map(record => ({
...record,
status: stringToPaymentRecordStatus(record.status),
test: Boolean(record.test),
})),
});

const refundRecords = await prisma.refundRecord.createMany({
data: generateRefundRecords(generatePaymentRecords(payments)).map(record => ({
...record,
status: stringToRefundRecordStatus(record.status),
test: Boolean(record.test),
})),
});

const productRecords = await prisma.product.createMany({
data: generateProductRecords(products).map(record => ({
...record,
Expand Down
17 changes: 17 additions & 0 deletions apps/backend-serverless/serverless.purple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ provider:
- PATCH
- DELETE
allowCredentials: true
iam:
role:
statements:
- Effect: Allow
Action:
- s3:GetObject
Resource: 'arn:aws:s3:::${self:custom.gas-bucket-name}/${self:custom.gas-object-name}'
stage: ${opt:stage, 'dev'}
region: us-east-1
environment:
Expand Down Expand Up @@ -159,6 +166,7 @@ functions:
Resource: ${self:resources.Outputs.ShopifyQueueArn.Value}
refund-transaction:
handler: src/handlers/transactions/refund-transaction.refundTransaction
timeout: 60 # Increase the timeout to 60 seconds
events:
- httpApi:
path: /refund-transaction
Expand Down Expand Up @@ -227,6 +235,11 @@ functions:
- httpApi:
path: /payment-status
method: get
iamRoleStatements:
- Effect: Allow
Action:
- s3:GetObject
Resource: 'arn:aws:s3:::${self:custom.gas-bucket-name}/${self:custom.gas-object-name}'
customer-data:
handler: src/handlers/clients/payment-ui/customer-data.customerData
events:
Expand All @@ -250,6 +263,10 @@ functions:
Action:
- sqs:SendMessage
Resource: ${self:resources.Outputs.ShopifyQueueArn.Value}
- Effect: Allow
Action:
- s3:GetObject
Resource: 'arn:aws:s3:::${self:custom.gas-bucket-name}/${self:custom.gas-object-name}'
refund-data:
handler: src/handlers/clients/merchant-ui/read-data/refund-data.refundData
events:
Expand Down
10 changes: 5 additions & 5 deletions apps/backend-serverless/src/handlers/shopify-handlers/refund.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,21 @@ export const refund = Sentry.AWSLambda.wrapHandler(
let usdcSize: number;

if (refundInitiation.test) {
usdcSize = 0;
usdcSize = 0.01;
} else {
usdcSize = await convertAmountAndCurrencyToUsdcSize(
refundInitiation.amount,
refundInitiation.currency,
axios,
axios
);
}

const newRefundRecordId = await generatePubkeyString();
await refundRecordService.createRefundRecord(
newRefundRecordId,
refundInitiation,
merchant,
usdcSize,
merchant.id,
usdcSize
);
} else {
throw error;
Expand All @@ -88,5 +88,5 @@ export const refund = Sentry.AWSLambda.wrapHandler(
},
{
rethrowAfterCapture: false,
},
}
);
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import { PaymentRecord, PrismaClient } from '@prisma/client';
import * as web3 from '@solana/web3.js';
import axios from 'axios';
import { ShopifyRefundInitiation } from '../../models/shopify/process-refund.request.model.js';
import { createPaymentProductNftsResponse } from '../../utilities/clients/create-payment-product-nfts-response.js';
import { delay } from '../../utilities/delay.utility.js';
import { constructTransaction, sendTransaction } from '../../utilities/transaction.utility.js';
import { convertAmountAndCurrencyToUsdcSize } from '../coin-gecko.service.js';
import { MerchantService } from '../database/merchant-service.database.service.js';
import { TransactionSignatureQuery } from '../database/payment-record-service.database.service.js';
import {
PaymentResolveResponse,
ShopifyResolveResponse,
getRecordServiceForTransaction,
} from '../database/record-service.database.service.js';
import { RefundRecordService } from '../database/refund-record-service.database.service.js';
import { TransactionRecordService } from '../database/transaction-record-service.database.service.js';
import { fetchGasKeypair } from '../fetch-gas-keypair.service.js';
import { fetchTransaction } from '../fetch-transaction.service.js';
import { mintCompressedNFT } from '../transaction-request/products-transaction.service.js';
import { WebSocketService } from '../websocket/send-websocket-message.service.js';

function getRandomArbitrary(min: number, max: number): number {
return Math.random() * (max - min) + min;
}

export const processTransaction = async (
signature: string,
prisma: PrismaClient,
Expand All @@ -31,6 +38,7 @@ export const processTransaction = async (
});

const recordService = await getRecordServiceForTransaction(transactionRecord, prisma);
const refundRecordService = new RefundRecordService(prisma);

const record = await recordService.getRecordFromTransactionRecord(transactionRecord);

Expand Down Expand Up @@ -104,5 +112,35 @@ export const processTransaction = async (
} catch (error) {
console.log('error minting compressed', error);
}
if (process.env.NODE_ENV === 'development') {
console.log('making the refund record dev');
const id = getRandomArbitrary(1, 1000000).toString();
const gid = getRandomArbitrary(1, 1000000).toString();

let refundInitiation: ShopifyRefundInitiation = {
id: gid,
gid: 'refundSession//' + gid,
payment_id: record.shopId,
amount: record.amount,
currency: record.currency,
test: record.test,
merchant_locale: 'us',
proposed_at: new Date().toISOString(),
};

let usdcSize: number;

if (refundInitiation.test) {
usdcSize = 0.01;
} else {
usdcSize = await convertAmountAndCurrencyToUsdcSize(
refundInitiation.amount,
refundInitiation.currency,
axios
);
}

await refundRecordService.createRefundRecord(id, refundInitiation, record.merchantId, usdcSize);
}
}
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
Merchant,
PaymentRecord,
PaymentRecordStatus,
PrismaClient,
Expand Down Expand Up @@ -299,7 +298,7 @@ export class RefundRecordService implements RecordService<RefundRecord, RefundRe
async createRefundRecord(
id: string,
refundInitiation: ShopifyRefundInitiation,
merchant: Merchant,
merchantId: string,
usdcAmount: number
): Promise<RefundRecord> {
return await prismaErrorHandler(
Expand All @@ -314,7 +313,7 @@ export class RefundRecordService implements RecordService<RefundRecord, RefundRe
shopGid: refundInitiation.gid,
shopPaymentId: refundInitiation.payment_id,
test: refundInitiation.test,
merchantId: merchant.id,
merchantId: merchantId,
transactionSignature: null,
requestedAt: new Date(),
completedAt: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { PaymentRecord, RefundRecord } from '@prisma/client';
import { TOKEN_PROGRAM_ID, decodeTransferCheckedInstruction } from '@solana/spl-token';
import axios from 'axios';
import { USDC_MINT } from '../../configs/tokens.config.js';
import { InvalidInputError } from '../../errors/invalid-input.error.js';
import {
TransactionRequestResponse,
parseAndValidateTransactionRequestResponse,
Expand All @@ -9,6 +11,7 @@ import { delay } from '../../utilities/delay.utility.js';
import { findPayingTokenAddressFromTransaction } from '../../utilities/transaction-inspection.utility.js';
import { buildRefundTransactionRequestEndpoint } from '../../utilities/transaction-request/endpoints.utility.js';
import { fetchTransaction } from '../fetch-transaction.service.js';
import { fetchBalance } from '../helius.service.js';

export const fetchRefundTransaction = async (
refundRecord: RefundRecord,
Expand All @@ -35,15 +38,28 @@ export const fetchRefundTransaction = async (
await delay(1000);
transaction = await fetchTransaction(associatedPaymentRecord.transactionSignature);
}
const transferInstruction = transaction.instructions[transaction.instructions.length - 2];

if (transferInstruction.programId.toBase58() != TOKEN_PROGRAM_ID.toBase58()) {
const error = new Error('Invalid transaction.' + transferInstruction.programId.toBase58());
throw error;
}

const decodedInstruction = decodeTransferCheckedInstruction(transferInstruction);

const mint = decodedInstruction.keys.mint;

const fetchBalancePromise = fetchBalance(account, mint.pubkey.toBase58());

const payingCustomerTokenAddress = await findPayingTokenAddressFromTransaction(transaction);

let receiverWalletAddress: string | null = null;
let receiverTokenAddress: string | null = payingCustomerTokenAddress.toBase58();

if (refundRecord.test) {
receiverWalletAddress = account;
receiverTokenAddress = null;
}
// if (refundRecord.test) {
// receiverWalletAddress = account;
// receiverTokenAddress = null;
// }

const endpoint = buildRefundTransactionRequestEndpoint(
receiverWalletAddress,
Expand All @@ -64,13 +80,21 @@ export const fetchRefundTransaction = async (
'Content-Type': 'application/json',
};

const response = await axiosInstance.post(endpoint, { headers: headers });
const axiosPostPromise = axiosInstance.post(endpoint, { headers: headers });

const [balance, axiosResponse] = await Promise.all([fetchBalancePromise, axiosPostPromise]);

// Check balance
if (balance < refundRecord.usdcAmount) {
throw new InvalidInputError('Not enough balance to refund');
}

if (response.status != 200) {
// Check axios response
if (axiosResponse.status != 200) {
throw new Error('Error fetching refund transaction.');
}

const transactionRequestResponse = parseAndValidateTransactionRequestResponse(response.data);
const transactionRequestResponse = parseAndValidateTransactionRequestResponse(axiosResponse.data);

return transactionRequestResponse;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { PaymentRecord, RefundRecord } from '@prisma/client';
import { TOKEN_PROGRAM_ID, decodeTransferCheckedInstruction } from '@solana/spl-token';
import { delay } from '../../utilities/delay.utility.js';
import { fetchTransaction } from '../fetch-transaction.service.js';
import { fetchBalance } from '../helius.service.js';

export async function checkRefundBalance(
associatedPaymentRecord: PaymentRecord,
refundRecord: RefundRecord,
account: string,
amount: number
) {
if (associatedPaymentRecord.transactionSignature == null) {
throw new Error('Payment transaction not found.');
}

let transaction;

while (transaction == null) {
await delay(1000);

transaction = await fetchTransaction(associatedPaymentRecord.transactionSignature);
}

const transferInstruction = transaction.instructions[transaction.instructions.length - 2];

if (transferInstruction.programId.toBase58() != TOKEN_PROGRAM_ID.toBase58()) {
const error = new Error('Invalid transaction.' + transferInstruction.programId.toBase58());
throw error;
}

const decodedInstruction = decodeTransferCheckedInstruction(transferInstruction);

const mint = decodedInstruction.keys.mint;

const balance = await fetchBalance(account, mint.pubkey.toBase58());

return balance >= refundRecord.usdcAmount;
}
Loading

3 comments on commit 63da4ea

@vercel
Copy link

@vercel vercel bot commented on 63da4ea Aug 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 63da4ea Aug 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 63da4ea Aug 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.