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

Commit 2331ffd

Browse files
refund balance checks
1 parent 7d5fb00 commit 2331ffd

File tree

9 files changed

+144
-97
lines changed

9 files changed

+144
-97
lines changed

apps/backend-serverless/prisma/seed.ts

Lines changed: 3 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ function generateMerchantRecords(count = 1): any[] {
5353
acceptedPrivacyPolicy: true,
5454
dismissCompleted: true,
5555
active: true,
56-
loyaltyProgram: 'tiers',
57-
pointsMint: 'Fq2oteAH3w4qKfDtnrdHTqVNRoUAWwMHSqeG7gsRqPSC',
58-
pointsBack: 1,
56+
// loyaltyProgram: 'tiers',
57+
// pointsMint: 'Fq2oteAH3w4qKfDtnrdHTqVNRoUAWwMHSqeG7gsRqPSC',
58+
// pointsBack: 1,
5959
};
6060
} else {
6161
merchant = {
@@ -80,65 +80,6 @@ function generateMerchantRecords(count = 1): any[] {
8080
return records;
8181
}
8282

83-
function generatePaymentRecords(count = 1): any[] {
84-
const records: any[] = [];
85-
for (let i = 0; i < count; i++) {
86-
const requestedAt = new Date();
87-
requestedAt.setDate(requestedAt.getDate() + i);
88-
const completedAt = new Date(requestedAt.getTime());
89-
completedAt.setDate(completedAt.getDate() + i + 1);
90-
91-
const record = {
92-
id: `payment-${i}`,
93-
status: 'completed',
94-
shopId: `r_2-${i}_shopid`,
95-
shopGid: `gid://shopify/PaymentSession/r_${i}_shopid`,
96-
shopGroup: `shop_group_${i}`,
97-
test: 1,
98-
amount: i + 1,
99-
currency: 'USD',
100-
usdcAmount: i + 1,
101-
cancelURL: `https://store${i}.myshopify.com/checkouts/c/randomId_-${i}/processing`,
102-
merchantId: `GZQN3FYe8WGLTWSBGDgprSfJmwwDrYNPL2vR2v9ZpJof`,
103-
transactionSignature: `317CdVpw26TCBpgKdaK8siAG3iMHatFPxph47GQieaZYojo9Q4qNG8vJ3r2EsHUWGEieEgzpFYBPmrqhiHh6sjLt`,
104-
requestedAt: requestedAt.toISOString(),
105-
completedAt: completedAt.toISOString(),
106-
};
107-
108-
records.push(record);
109-
}
110-
return records;
111-
}
112-
113-
function generateRefundRecords(paymentRecords: any[]): any[] {
114-
const records: any[] = [];
115-
116-
for (let i = 0; i < paymentRecords.length; i++) {
117-
const paymentRecord = paymentRecords[i];
118-
const requestedAt = new Date(paymentRecord.requestedAt);
119-
const completedAt = new Date(paymentRecord.completedAt);
120-
121-
const record = {
122-
id: `refund-${i}`,
123-
status: i % 2 === 0 ? 'pending' : 'completed',
124-
amount: i,
125-
currency: 'USD',
126-
usdcAmount: i,
127-
shopId: `r_${i}_shopid`,
128-
shopGid: `gid://shopify/PaymentSession/r_${i}_shopid`,
129-
shopPaymentId: paymentRecord.shopId,
130-
test: 1,
131-
merchantId: paymentRecord.merchantId,
132-
transactionSignature: i % 2 === 0 ? null : `signature-${i}`,
133-
requestedAt: requestedAt.toISOString(),
134-
completedAt: i % 2 === 0 ? null : completedAt.toISOString(),
135-
};
136-
137-
records.push(record);
138-
}
139-
return records;
140-
}
141-
14283
function generateProductRecords(count = 2): any[] {
14384
const records: any[] = [
14485
{
@@ -199,22 +140,6 @@ async function insertGeneratedData(merchants: number, payments: number, products
199140
})),
200141
});
201142

202-
const paymentRecords = await prisma.paymentRecord.createMany({
203-
data: generatePaymentRecords(payments).map(record => ({
204-
...record,
205-
status: stringToPaymentRecordStatus(record.status),
206-
test: Boolean(record.test),
207-
})),
208-
});
209-
210-
const refundRecords = await prisma.refundRecord.createMany({
211-
data: generateRefundRecords(generatePaymentRecords(payments)).map(record => ({
212-
...record,
213-
status: stringToRefundRecordStatus(record.status),
214-
test: Boolean(record.test),
215-
})),
216-
});
217-
218143
const productRecords = await prisma.product.createMany({
219144
data: generateProductRecords(products).map(record => ({
220145
...record,

apps/backend-serverless/serverless.purple.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ provider:
2424
- PATCH
2525
- DELETE
2626
allowCredentials: true
27+
iam:
28+
role:
29+
statements:
30+
- Effect: Allow
31+
Action:
32+
- s3:GetObject
33+
Resource: 'arn:aws:s3:::${self:custom.gas-bucket-name}/${self:custom.gas-object-name}'
2734
stage: ${opt:stage, 'dev'}
2835
region: us-east-1
2936
environment:
@@ -159,6 +166,7 @@ functions:
159166
Resource: ${self:resources.Outputs.ShopifyQueueArn.Value}
160167
refund-transaction:
161168
handler: src/handlers/transactions/refund-transaction.refundTransaction
169+
timeout: 60 # Increase the timeout to 60 seconds
162170
events:
163171
- httpApi:
164172
path: /refund-transaction
@@ -227,6 +235,11 @@ functions:
227235
- httpApi:
228236
path: /payment-status
229237
method: get
238+
iamRoleStatements:
239+
- Effect: Allow
240+
Action:
241+
- s3:GetObject
242+
Resource: 'arn:aws:s3:::${self:custom.gas-bucket-name}/${self:custom.gas-object-name}'
230243
customer-data:
231244
handler: src/handlers/clients/payment-ui/customer-data.customerData
232245
events:
@@ -250,6 +263,10 @@ functions:
250263
Action:
251264
- sqs:SendMessage
252265
Resource: ${self:resources.Outputs.ShopifyQueueArn.Value}
266+
- Effect: Allow
267+
Action:
268+
- s3:GetObject
269+
Resource: 'arn:aws:s3:::${self:custom.gas-bucket-name}/${self:custom.gas-object-name}'
253270
refund-data:
254271
handler: src/handlers/clients/merchant-ui/read-data/refund-data.refundData
255272
events:

apps/backend-serverless/src/handlers/shopify-handlers/refund.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,21 @@ export const refund = Sentry.AWSLambda.wrapHandler(
5656
let usdcSize: number;
5757

5858
if (refundInitiation.test) {
59-
usdcSize = 0;
59+
usdcSize = 0.01;
6060
} else {
6161
usdcSize = await convertAmountAndCurrencyToUsdcSize(
6262
refundInitiation.amount,
6363
refundInitiation.currency,
64-
axios,
64+
axios
6565
);
6666
}
6767

6868
const newRefundRecordId = await generatePubkeyString();
6969
await refundRecordService.createRefundRecord(
7070
newRefundRecordId,
7171
refundInitiation,
72-
merchant,
73-
usdcSize,
72+
merchant.id,
73+
usdcSize
7474
);
7575
} else {
7676
throw error;
@@ -88,5 +88,5 @@ export const refund = Sentry.AWSLambda.wrapHandler(
8888
},
8989
{
9090
rethrowAfterCapture: false,
91-
},
91+
}
9292
);

apps/backend-serverless/src/services/business-logic/process-transaction.service.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
11
import { PaymentRecord, PrismaClient } from '@prisma/client';
22
import * as web3 from '@solana/web3.js';
33
import axios from 'axios';
4+
import { ShopifyRefundInitiation } from '../../models/shopify/process-refund.request.model.js';
45
import { createPaymentProductNftsResponse } from '../../utilities/clients/create-payment-product-nfts-response.js';
56
import { delay } from '../../utilities/delay.utility.js';
67
import { constructTransaction, sendTransaction } from '../../utilities/transaction.utility.js';
8+
import { convertAmountAndCurrencyToUsdcSize } from '../coin-gecko.service.js';
79
import { MerchantService } from '../database/merchant-service.database.service.js';
810
import { TransactionSignatureQuery } from '../database/payment-record-service.database.service.js';
911
import {
1012
PaymentResolveResponse,
1113
ShopifyResolveResponse,
1214
getRecordServiceForTransaction,
1315
} from '../database/record-service.database.service.js';
16+
import { RefundRecordService } from '../database/refund-record-service.database.service.js';
1417
import { TransactionRecordService } from '../database/transaction-record-service.database.service.js';
1518
import { fetchGasKeypair } from '../fetch-gas-keypair.service.js';
1619
import { fetchTransaction } from '../fetch-transaction.service.js';
1720
import { mintCompressedNFT } from '../transaction-request/products-transaction.service.js';
1821
import { WebSocketService } from '../websocket/send-websocket-message.service.js';
1922

23+
function getRandomArbitrary(min: number, max: number): number {
24+
return Math.random() * (max - min) + min;
25+
}
26+
2027
export const processTransaction = async (
2128
signature: string,
2229
prisma: PrismaClient,
@@ -31,6 +38,7 @@ export const processTransaction = async (
3138
});
3239

3340
const recordService = await getRecordServiceForTransaction(transactionRecord, prisma);
41+
const refundRecordService = new RefundRecordService(prisma);
3442

3543
const record = await recordService.getRecordFromTransactionRecord(transactionRecord);
3644

@@ -104,5 +112,35 @@ export const processTransaction = async (
104112
} catch (error) {
105113
console.log('error minting compressed', error);
106114
}
115+
if (process.env.NODE_ENV === 'development') {
116+
console.log('making the refund record dev');
117+
const id = getRandomArbitrary(1, 1000000).toString();
118+
const gid = getRandomArbitrary(1, 1000000).toString();
119+
120+
let refundInitiation: ShopifyRefundInitiation = {
121+
id: gid,
122+
gid: 'refundSession//' + gid,
123+
payment_id: record.shopId,
124+
amount: record.amount,
125+
currency: record.currency,
126+
test: record.test,
127+
merchant_locale: 'us',
128+
proposed_at: new Date().toISOString(),
129+
};
130+
131+
let usdcSize: number;
132+
133+
if (refundInitiation.test) {
134+
usdcSize = 0.01;
135+
} else {
136+
usdcSize = await convertAmountAndCurrencyToUsdcSize(
137+
refundInitiation.amount,
138+
refundInitiation.currency,
139+
axios
140+
);
141+
}
142+
143+
await refundRecordService.createRefundRecord(id, refundInitiation, record.merchantId, usdcSize);
144+
}
107145
}
108146
};

apps/backend-serverless/src/services/database/refund-record-service.database.service.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
Merchant,
32
PaymentRecord,
43
PaymentRecordStatus,
54
PrismaClient,
@@ -299,7 +298,7 @@ export class RefundRecordService implements RecordService<RefundRecord, RefundRe
299298
async createRefundRecord(
300299
id: string,
301300
refundInitiation: ShopifyRefundInitiation,
302-
merchant: Merchant,
301+
merchantId: string,
303302
usdcAmount: number
304303
): Promise<RefundRecord> {
305304
return await prismaErrorHandler(
@@ -314,7 +313,7 @@ export class RefundRecordService implements RecordService<RefundRecord, RefundRe
314313
shopGid: refundInitiation.gid,
315314
shopPaymentId: refundInitiation.payment_id,
316315
test: refundInitiation.test,
317-
merchantId: merchant.id,
316+
merchantId: merchantId,
318317
transactionSignature: null,
319318
requestedAt: new Date(),
320319
completedAt: null,

apps/backend-serverless/src/services/transaction-request/fetch-refund-transaction.service.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { PaymentRecord, RefundRecord } from '@prisma/client';
2+
import { TOKEN_PROGRAM_ID, decodeTransferCheckedInstruction } from '@solana/spl-token';
23
import axios from 'axios';
34
import { USDC_MINT } from '../../configs/tokens.config.js';
5+
import { InvalidInputError } from '../../errors/invalid-input.error.js';
46
import {
57
TransactionRequestResponse,
68
parseAndValidateTransactionRequestResponse,
@@ -9,6 +11,7 @@ import { delay } from '../../utilities/delay.utility.js';
911
import { findPayingTokenAddressFromTransaction } from '../../utilities/transaction-inspection.utility.js';
1012
import { buildRefundTransactionRequestEndpoint } from '../../utilities/transaction-request/endpoints.utility.js';
1113
import { fetchTransaction } from '../fetch-transaction.service.js';
14+
import { fetchBalance } from '../helius.service.js';
1215

1316
export const fetchRefundTransaction = async (
1417
refundRecord: RefundRecord,
@@ -35,6 +38,19 @@ export const fetchRefundTransaction = async (
3538
await delay(1000);
3639
transaction = await fetchTransaction(associatedPaymentRecord.transactionSignature);
3740
}
41+
const transferInstruction = transaction.instructions[transaction.instructions.length - 2];
42+
43+
if (transferInstruction.programId.toBase58() != TOKEN_PROGRAM_ID.toBase58()) {
44+
const error = new Error('Invalid transaction.' + transferInstruction.programId.toBase58());
45+
throw error;
46+
}
47+
48+
const decodedInstruction = decodeTransferCheckedInstruction(transferInstruction);
49+
50+
const mint = decodedInstruction.keys.mint;
51+
52+
const fetchBalancePromise = fetchBalance(account, mint.pubkey.toBase58());
53+
3854
const payingCustomerTokenAddress = await findPayingTokenAddressFromTransaction(transaction);
3955

4056
let receiverWalletAddress: string | null = null;
@@ -64,13 +80,21 @@ export const fetchRefundTransaction = async (
6480
'Content-Type': 'application/json',
6581
};
6682

67-
const response = await axiosInstance.post(endpoint, { headers: headers });
83+
const axiosPostPromise = axiosInstance.post(endpoint, { headers: headers });
84+
85+
const [balance, axiosResponse] = await Promise.all([fetchBalancePromise, axiosPostPromise]);
86+
87+
// Check balance
88+
if (balance < refundRecord.usdcAmount) {
89+
throw new InvalidInputError('Not enough balance to refund');
90+
}
6891

69-
if (response.status != 200) {
92+
// Check axios response
93+
if (axiosResponse.status != 200) {
7094
throw new Error('Error fetching refund transaction.');
7195
}
7296

73-
const transactionRequestResponse = parseAndValidateTransactionRequestResponse(response.data);
97+
const transactionRequestResponse = parseAndValidateTransactionRequestResponse(axiosResponse.data);
7498

7599
return transactionRequestResponse;
76100
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { PaymentRecord, RefundRecord } from '@prisma/client';
2+
import { TOKEN_PROGRAM_ID, decodeTransferCheckedInstruction } from '@solana/spl-token';
3+
import { delay } from '../../utilities/delay.utility.js';
4+
import { fetchTransaction } from '../fetch-transaction.service.js';
5+
import { fetchBalance } from '../helius.service.js';
6+
7+
export async function checkRefundBalance(
8+
associatedPaymentRecord: PaymentRecord,
9+
refundRecord: RefundRecord,
10+
account: string,
11+
amount: number
12+
) {
13+
if (associatedPaymentRecord.transactionSignature == null) {
14+
throw new Error('Payment transaction not found.');
15+
}
16+
17+
let transaction;
18+
19+
while (transaction == null) {
20+
await delay(1000);
21+
22+
transaction = await fetchTransaction(associatedPaymentRecord.transactionSignature);
23+
}
24+
25+
const transferInstruction = transaction.instructions[transaction.instructions.length - 2];
26+
27+
if (transferInstruction.programId.toBase58() != TOKEN_PROGRAM_ID.toBase58()) {
28+
const error = new Error('Invalid transaction.' + transferInstruction.programId.toBase58());
29+
throw error;
30+
}
31+
32+
const decodedInstruction = decodeTransferCheckedInstruction(transferInstruction);
33+
34+
const mint = decodedInstruction.keys.mint;
35+
36+
const balance = await fetchBalance(account, mint.pubkey.toBase58());
37+
38+
return balance >= refundRecord.usdcAmount;
39+
}

apps/merchant-ui/src/components/OpenRefunds.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ export function OpenRefunds(props: Props) {
9595
credentials: 'include',
9696
});
9797
const data = await response.json();
98-
if (!response.ok) {
99-
throw new Error(`HTTP error! status: ${response.status}`);
98+
if (response.status !== 200) {
99+
throw new Error(`${data.error}`);
100100
}
101101

102102
const buffer = Buffer.from(data.transaction, 'base64');
@@ -110,7 +110,7 @@ export function OpenRefunds(props: Props) {
110110
});
111111
const statusData = await statusResponse.json();
112112
if (!statusResponse.ok) {
113-
throw new Error(`HTTP error! status: ${statusResponse.status}`);
113+
throw new Error(`${statusData.error}`);
114114
}
115115
await new Promise(resolve => setTimeout(resolve, 500));
116116
if (statusData.refundStatus.status !== RefundStatus.Pending) {

0 commit comments

Comments
 (0)