Skip to content

Commit

Permalink
Merge pull request #308 from OfflineHQ/307-unlock-mint-loyalty-card-u…
Browse files Browse the repository at this point in the history
…pon-creating-linked-customer

🔧 Update SHOPIFY_API_SECRET in .env.local for new dev app credentials
  • Loading branch information
sebpalluel authored Jun 25, 2024
2 parents 4e6df66 + 2ea427c commit 6c1fc7f
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 45 deletions.
4 changes: 2 additions & 2 deletions .env.local
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,5 @@ COMETH_CONNECT_API_KEY=vDMJtXRUsdDVCXJ0GBAaKMTjuebI5S3Y
NEXT_PUBLIC_WC_PROJECT_ID=68b34422801cb3e8ea1eb7f823266c28
NEXT_PUBLIC_WC_RELAY_URL=wss://relay.walletconnect.com

# Shopify
SHOPIFY_API_SECRET=c886ebdff67650455049c4cc52517c0d
# Shopify (secret from dev app shopify)
SHOPIFY_API_SECRET=632c4c9ce9defab5f1611d1cad914d3b
12 changes: 0 additions & 12 deletions apps/web/vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,9 @@
"installCommand": "pnpm install",
"ignoreCommand": "node ../../tools/scripts/vercel-ignore.js",
"crons": [
{
"path": "/apps/web/crons/handlePendingOrders.ts",
"schedule": "*/5 * * * *"
},
{
"path": "/apps/web/crons/setRates.ts",
"schedule": "0 */12 * * *"
},
{
"path": "/apps/web/crons/processRedisOrders.ts",
"schedule": "* * * * *"
},
{
"path": "/apps/web/crons/processLoyaltyCardsMint.ts",
"schedule": "*/5 * * * *"
}
],
"rewrites": [
Expand Down
16 changes: 14 additions & 2 deletions libs/gql/admin/api/src/generated/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,15 @@ ${EventParametersFieldsFragmentDoc}`;
) {
loyaltyCardId
}
}
`;
const GetLoyaltyCardNftContractAddressForOrganizerDocument = `
query GetLoyaltyCardNftContractAddressForOrganizer($organizerId: String!, $chainId: String!) {
loyaltyCardNftContract(
where: {organizerId: {_eq: $organizerId}, chainId: {_eq: $chainId}}
) {
contractAddress
}
}
`;
const GetLoyaltyCardByContractAddressForProcessDocument = `
Expand Down Expand Up @@ -1626,8 +1635,8 @@ ${EventPassFieldsFragmentDoc}`;
}
}
${StripeCustomerFieldsFragmentDoc}`;
export type Requester<C = {}, E = unknown> = <R, V>(doc: string, vars?: V, options?: C) => Promise<R> | AsyncIterable<R>
export function getSdk<C, E>(requester: Requester<C, E>) {
export type Requester<C = {}> = <R, V>(doc: string, vars?: V, options?: C) => Promise<R> | AsyncIterable<R>
export function getSdk<C>(requester: Requester<C>) {
return {
UpdateAccount(variables: Types.UpdateAccountMutationVariables, options?: C): Promise<Types.UpdateAccountMutation> {
return requester<Types.UpdateAccountMutation, Types.UpdateAccountMutationVariables>(UpdateAccountDocument, variables, options) as Promise<Types.UpdateAccountMutation>;
Expand Down Expand Up @@ -1833,6 +1842,9 @@ export function getSdk<C, E>(requester: Requester<C, E>) {
GetLoyaltyCardNftContractByContractAddress(variables: Types.GetLoyaltyCardNftContractByContractAddressQueryVariables, options?: C): Promise<Types.GetLoyaltyCardNftContractByContractAddressQuery> {
return requester<Types.GetLoyaltyCardNftContractByContractAddressQuery, Types.GetLoyaltyCardNftContractByContractAddressQueryVariables>(GetLoyaltyCardNftContractByContractAddressDocument, variables, options) as Promise<Types.GetLoyaltyCardNftContractByContractAddressQuery>;
},
GetLoyaltyCardNftContractAddressForOrganizer(variables: Types.GetLoyaltyCardNftContractAddressForOrganizerQueryVariables, options?: C): Promise<Types.GetLoyaltyCardNftContractAddressForOrganizerQuery> {
return requester<Types.GetLoyaltyCardNftContractAddressForOrganizerQuery, Types.GetLoyaltyCardNftContractAddressForOrganizerQueryVariables>(GetLoyaltyCardNftContractAddressForOrganizerDocument, variables, options) as Promise<Types.GetLoyaltyCardNftContractAddressForOrganizerQuery>;
},
GetLoyaltyCardByContractAddressForProcess(variables?: Types.GetLoyaltyCardByContractAddressForProcessQueryVariables, options?: C): Promise<Types.GetLoyaltyCardByContractAddressForProcessQuery> {
return requester<Types.GetLoyaltyCardByContractAddressForProcessQuery, Types.GetLoyaltyCardByContractAddressForProcessQueryVariables>(GetLoyaltyCardByContractAddressForProcessDocument, variables, options) as Promise<Types.GetLoyaltyCardByContractAddressForProcessQuery>;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ query GetLoyaltyCardNftContractByContractAddress(
}
}

query GetLoyaltyCardNftContractAddressForOrganizer(
$organizerId: String!
$chainId: String!
) {
loyaltyCardNftContract(
where: { organizerId: { _eq: $organizerId }, chainId: { _eq: $chainId } }
) {
contractAddress
}
}

query GetLoyaltyCardByContractAddressForProcess {
loyaltyCardNft(
where: { status: { _in: [CONFIRMED, ERROR] } }
Expand Down
8 changes: 8 additions & 0 deletions libs/gql/admin/types/src/generated/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,14 @@ export type GetLoyaltyCardNftContractByContractAddressQueryVariables = Types.Exa

export type GetLoyaltyCardNftContractByContractAddressQuery = { __typename?: 'query_root', loyaltyCardNftContract: Array<{ __typename?: 'loyaltyCardNftContract', loyaltyCardId: string }> };

export type GetLoyaltyCardNftContractAddressForOrganizerQueryVariables = Types.Exact<{
organizerId: Types.Scalars['String']['input'];
chainId: Types.Scalars['String']['input'];
}>;


export type GetLoyaltyCardNftContractAddressForOrganizerQuery = { __typename?: 'query_root', loyaltyCardNftContract: Array<{ __typename?: 'loyaltyCardNftContract', contractAddress: string }> };

export type GetLoyaltyCardByContractAddressForProcessQueryVariables = Types.Exact<{ [key: string]: never; }>;


Expand Down
4 changes: 2 additions & 2 deletions libs/gql/anonymous/api/src/generated/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ export const EventPassNftFieldsFragmentDoc = `
}
${EventPassNftFieldsFragmentDoc}
${EventPassFieldsFragmentDoc}`;
export type Requester<C = {}, E = unknown> = <R, V>(doc: string, vars?: V, options?: C) => Promise<R> | AsyncIterable<R>
export function getSdk<C, E>(requester: Requester<C, E>) {
export type Requester<C = {}> = <R, V>(doc: string, vars?: V, options?: C) => Promise<R> | AsyncIterable<R>
export function getSdk<C>(requester: Requester<C>) {
return {
GetEventPassNftByTokenReference(variables: Types.GetEventPassNftByTokenReferenceQueryVariables, options?: C): Promise<Types.GetEventPassNftByTokenReferenceQuery> {
return requester<Types.GetEventPassNftByTokenReferenceQuery, Types.GetEventPassNftByTokenReferenceQueryVariables>(GetEventPassNftByTokenReferenceDocument, variables, options) as Promise<Types.GetEventPassNftByTokenReferenceQuery>;
Expand Down
4 changes: 2 additions & 2 deletions libs/gql/user/api/src/generated/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,8 +528,8 @@ ${OrganizerFieldsFragmentDoc}`;
}
}
`;
export type Requester<C = {}, E = unknown> = <R, V>(doc: string, vars?: V, options?: C) => Promise<R> | AsyncIterable<R>
export function getSdk<C, E>(requester: Requester<C, E>) {
export type Requester<C = {}> = <R, V>(doc: string, vars?: V, options?: C) => Promise<R> | AsyncIterable<R>
export function getSdk<C>(requester: Requester<C>) {
return {
GetAccount(variables: Types.GetAccountQueryVariables, options?: C): Promise<Types.GetAccountQuery> {
return requester<Types.GetAccountQuery, Types.GetAccountQueryVariables>(GetAccountDocument, variables, options) as Promise<Types.GetAccountQuery>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jest.mock('@nft/loyalty-card', () => ({
LoyaltyCardNftWrapper: jest.fn().mockImplementation(() => ({
mintWithPassword: jest.fn(),
mint: jest.fn(),
getLoyaltyCardNftContractAddressForOrganizer: jest.fn(),
setAsMinted: jest.fn(),
getLoyaltyCardOwnedByAddress: jest.fn(),
})),
Expand Down Expand Up @@ -543,11 +544,13 @@ describe('ShopifyWebhookAndApiHandler', () => {
});
});
describe('ShopifyWebhookAndApiHandler - createShopifyCustomer', () => {
let mockLoyaltyCardSdk: LoyaltyCardNftWrapper;
let handler: ShopifyWebhookAndApiHandler;
let mockRequest: NextRequest;

beforeEach(() => {
handler = new ShopifyWebhookAndApiHandler();
mockLoyaltyCardSdk = new LoyaltyCardNftWrapper();
mockRequest = createMockRequest(
new URLSearchParams({
address: 'test-address',
Expand All @@ -567,17 +570,38 @@ describe('ShopifyWebhookAndApiHandler', () => {
(adminSdk.GetShopifyCustomer as jest.Mock).mockResolvedValue({
shopifyCustomer: [],
});

(
mockLoyaltyCardSdk.getLoyaltyCardNftContractAddressForOrganizer as jest.Mock
).mockResolvedValue('test-contract');

(mockLoyaltyCardSdk.mint as jest.Mock).mockResolvedValue({
success: true,
});
});

it('should create a new Shopify customer', async () => {
(adminSdk.InsertShopifyCustomer as jest.Mock).mockResolvedValue({});
const response = await handler.createShopifyCustomer({
req: mockRequest,
id: 'test-customer-id',
loyaltyCardSdk: mockLoyaltyCardSdk,
});
expect(handleAccount as jest.Mock).toHaveBeenCalledWith({
address: 'test-address',
});
expect(
mockLoyaltyCardSdk.getLoyaltyCardNftContractAddressForOrganizer,
).toHaveBeenCalledWith({
organizerId: 'test-organizer-id',
chainId: getCurrentChain().chainIdHex,
});
expect(mockLoyaltyCardSdk.mint).toHaveBeenCalledWith({
contractAddress: 'test-contract',
ownerAddress: 'test-address',
chainId: getCurrentChain().chainIdHex,
organizerId: 'test-organizer-id',
});
expect(response.status).toBe(200);
expect(adminSdk.InsertShopifyCustomer).toHaveBeenCalledWith({
object: {
Expand All @@ -588,7 +612,7 @@ describe('ShopifyWebhookAndApiHandler', () => {
});
});

it('should throw BadRequestError if the customer already exists', async () => {
it('should not create customer if the customer already exists', async () => {
(adminSdk.GetShopifyCustomer as jest.Mock).mockResolvedValue({
shopifyCustomer: [{ address: 'test-address' }],
});
Expand All @@ -597,13 +621,30 @@ describe('ShopifyWebhookAndApiHandler', () => {
const response = await handler.createShopifyCustomer({
req: mockRequest,
id: 'test-customer-id',
loyaltyCardSdk: mockLoyaltyCardSdk,
});

expect(response.status).toBe(400);
expect(response.status).toBe(200);
expect(adminSdk.InsertShopifyCustomer).not.toHaveBeenCalled();
expect(
mockLoyaltyCardSdk.getLoyaltyCardNftContractAddressForOrganizer,
).toHaveBeenCalled();
expect(mockLoyaltyCardSdk.mint).toHaveBeenCalled();
});

it('should throw InternalServerError if the contract address is not found', async () => {
(
mockLoyaltyCardSdk.getLoyaltyCardNftContractAddressForOrganizer as jest.Mock
).mockResolvedValue(null);

const response = await handler.createShopifyCustomer({
req: mockRequest,
id: 'test-customer-id',
loyaltyCardSdk: mockLoyaltyCardSdk,
});
expect(response.status).toBe(500);
expect(JSON.parse(response.body)).toEqual(
expect.objectContaining({
error: expect.stringContaining('Customer already exists'),
error: expect.stringContaining('Internal Server Error'),
}),
);
});
Expand Down
66 changes: 45 additions & 21 deletions libs/integrations/external-api-handlers/src/lib/shopify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type HasLoyaltyCardOptions = MintLoyaltyCardOptions;

export interface CreateShopifyCustomerOptions extends ApiHandlerOptions {
id: string;
loyaltyCardSdk?: LoyaltyCardNftWrapper;
}

export type GetShopifyCustomerOptions = CreateShopifyCustomerOptions;
Expand Down Expand Up @@ -121,7 +122,6 @@ export class ShopifyWebhookAndApiHandler extends BaseWebhookAndApiHandler {
'Not Authorized: ' + getErrorMessage(error),
);
});
console.log({ resultParams, organizerId });
const validatedParams = await this.serializeAndValidateParams(
requestType,
resultParams,
Expand Down Expand Up @@ -291,7 +291,6 @@ export class ShopifyWebhookAndApiHandler extends BaseWebhookAndApiHandler {
req,
RequestType.HasLoyaltyCard,
);
console.log({ ownerAddress, organizerId });
const loyaltyCard = await loyaltyCardSdk
.getLoyaltyCardOwnedByAddress({
ownerAddress: ownerAddress.toLowerCase(),
Expand All @@ -300,18 +299,13 @@ export class ShopifyWebhookAndApiHandler extends BaseWebhookAndApiHandler {
organizerId,
})
.catch((error: Error) => {
console.log({ error });
console.error(
`Error checking NFT existence: ${getErrorMessage(error)}`,
);
throw new InternalServerError(
`Error checking NFT existence: ${getErrorMessage(error)}`,
);
});
console.log({
loyaltyCard,
res: JSON.stringify({ isOwned: !!loyaltyCard }),
});
return new NextResponse(JSON.stringify({ isOwned: !!loyaltyCard }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
Expand All @@ -327,30 +321,60 @@ export class ShopifyWebhookAndApiHandler extends BaseWebhookAndApiHandler {
req,
RequestType.CreateShopifyCustomer,
);
console.log({ address, organizerId });
const shopifyCustomer = await this.getShopifyCustomer({
organizerId,
customerId: id,
});
if (shopifyCustomer) {
throw new BadRequestError('Customer already exists');
}
// get or create a new account
await handleAccount({
address: address.toLowerCase(),
});
await adminSdk
.InsertShopifyCustomer({
object: {
organizerId,
customerId: id,
address: address.toLowerCase(),
},
if (!shopifyCustomer) {
await adminSdk
.InsertShopifyCustomer({
object: {
organizerId,
customerId: id,
address: address.toLowerCase(),
},
})
.catch((error: Error) => {
throw new InternalServerError(
`Error creating shopify customer: ${getErrorMessage(error)}`,
);
});
}
const loyaltyCardSdk =
options.loyaltyCardSdk || new LoyaltyCardNftWrapper();

const contractAddress =
await loyaltyCardSdk.getLoyaltyCardNftContractAddressForOrganizer({
organizerId,
chainId: getCurrentChain().chainIdHex,
});
if (!contractAddress) {
throw new InternalServerError('No contract address found');
}
await loyaltyCardSdk
.mint({
contractAddress: contractAddress.toLowerCase(),
ownerAddress: address.toLowerCase(),
chainId: getCurrentChain().chainIdHex,
organizerId,
})
.catch((error: Error) => {
throw new InternalServerError(
`Error creating shopify customer: ${getErrorMessage(error)}`,
);
// Check if the error is already one of our custom errors
if (error instanceof CustomError) {
throw error; // It's already a custom error, re-throw it
} else {
// It's not one of our custom errors, wrap it in a custom error class
console.error(
`Error minting loyalty card: ${getErrorMessage(error)}`,
);
throw new InternalServerError(
`Error minting loyalty card: ${getErrorMessage(error)}`,
);
}
});
return new NextResponse(JSON.stringify({}), {
status: 200,
Expand Down
8 changes: 8 additions & 0 deletions libs/nft/loyalty-card/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { adminSdk } from '@gql/admin/api';
import {
GetLoyaltyCardNftContractByContractAddressQueryVariables,
GetLoyaltyCardOwnedByAddressQueryVariables,
type GetLoyaltyCardNftContractAddressForOrganizerQueryVariables,
} from '@gql/admin/types';
import { LoyaltyCardNft_Set_Input, NftStatus_Enum } from '@gql/shared/types';
import { BadRequestError, NotFoundError } from '@next/api-handler';
Expand All @@ -24,6 +25,13 @@ export class LoyaltyCardNftWrapper {
this.adminSdk = adminSdk;
this.mintPasswordNftWrapper = new MintPasswordNftWrapper();
}
async getLoyaltyCardNftContractAddressForOrganizer(
props: GetLoyaltyCardNftContractAddressForOrganizerQueryVariables,
) {
const res =
await this.adminSdk.GetLoyaltyCardNftContractAddressForOrganizer(props);
return res.loyaltyCardNftContract?.[0]?.contractAddress;
}
async getLoyaltyCardOwnedByAddress(
props: GetLoyaltyCardOwnedByAddressQueryVariables,
) {
Expand Down

0 comments on commit 6c1fc7f

Please sign in to comment.