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

Commit

Permalink
UI loyalty data updating (#479)
Browse files Browse the repository at this point in the history
* loyalty updating works

* updating product activated status

* Able to add and auto sorting tiers
  • Loading branch information
harshasomisetty authored Jul 27, 2023
1 parent c38381e commit fcae9dd
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 143 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { InvalidInputError } from '../../../../errors/invalid-input.error.js';
import { parseAndValidatePaymentAddressRequestBody } from '../../../../models/clients/merchant-ui/payment-address-request.model.js';
import { contingentlyHandleAppConfigure } from '../../../../services/business-logic/contigently-handle-app-configure.service.js';
import { MerchantService, MerchantUpdate } from '../../../../services/database/merchant-service.database.service.js';
import { createGeneralResponse } from '../../../../utilities/clients/merchant-ui/create-general-response.js';
import { createOnboardingResponse } from '../../../../utilities/clients/merchant-ui/create-onboarding-response.utility.js';
import { withAuth } from '../../../../utilities/clients/merchant-ui/token-authenticate.utility.js';
import { syncKybState } from '../../../../utilities/persona/sync-kyb-status.js';
import { createErrorResponse } from '../../../../utilities/responses/error-response.utility.js';
Expand Down Expand Up @@ -50,8 +48,12 @@ export const updateMerchant = Sentry.AWSLambda.wrapHandler(
'pointsBack',
];

if (keysToCheck.every(key => merchantUpdateRequest[key] == null)) {
throw new InvalidInputError('no relevant fields in request body');
if (
keysToCheck.every(key => merchantUpdateRequest[key] == null) &&
merchantUpdateRequest.tier == null &&
merchantUpdateRequest.product == null
) {
throw new InvalidInputError('No fields to update in request body');
}

let merchantUpdateQuery = {};
Expand All @@ -71,43 +73,36 @@ export const updateMerchant = Sentry.AWSLambda.wrapHandler(
merchant = await merchantService.updateMerchant(merchant, merchantUpdateQuery as MerchantUpdate);
}

if (
merchant.kybInquiry &&
merchant.kybState !== KybState.finished &&
merchant.kybState !== KybState.failed
) {
try {
try {
if (
merchant.kybInquiry &&
merchant.kybState !== KybState.finished &&
merchant.kybState !== KybState.failed
) {
merchant = await syncKybState(merchant, prisma);
} catch {
// it's unlikely that this will throw but we should catch and record all errors underneath this
// we don't need to error out here because a new merchant shouldn't have a kyb inquirey but if they do
// we don't wana disrupt the flow, they'll just get blocked elsewhere
} else if (merchant.kybState === KybState.finished) {
merchant = await contingentlyHandleAppConfigure(merchant, axios, prisma);
}
} catch (error) {
// it's unlikely that this will throw but we should catch and record all errors underneath this
// we don't need to error out here because a new merchant shouldn't have a kyb inquirey but if they do
// we don't wana disrupt the flow, they'll just get blocked elsewhere
console.log('error with kyb');

Sentry.captureException(error);
await Sentry.flush(2000);
}

if (merchant.kybState === KybState.finished) {
try {
merchant = await contingentlyHandleAppConfigure(merchant, axios, prisma);
} catch {
// It's possible for this to throw but we should capture and log alll errors underneath this
// It's better if we just return the merchant data here and handle the issue elsewhere
}
if (merchantUpdateRequest.tier && Object.keys(merchantUpdateRequest.tier).length != 0) {
await merchantService.upsertTier(merchant.id, merchantUpdateRequest.tier);
}

if (merchantUpdateRequest.product && Object.keys(merchantUpdateRequest.product).length != 0) {
await merchantService.toggleProduct(merchantUpdateRequest.product);
}
const generalResponse = await createGeneralResponse(merchantAuthToken, prisma);
const onboardingResponse = createOnboardingResponse(merchant);

const responseBodyData = {
merchantData: {
name: merchant.name,
paymentAddress: merchant.walletAddress ?? merchant.tokenAddress,
onboarding: onboardingResponse,
},
general: generalResponse,
};

return {
statusCode: 200,
body: JSON.stringify(responseBodyData),
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ export const merchantUpdateRequestBodySchema = object().shape({
loyaltyProgram: string().optional(),
pointsMint: string().optional(),
pointsBack: number().optional(),
tier: object()
.shape({
id: number().optional(),
name: string().optional(),
threshold: number().optional(),
discount: number().optional(),
active: boolean().optional(),
})
.optional(),
product: object()
.shape({
productId: string().optional(),
active: boolean().optional(),
})
.optional(),
});

export type MerchantUpdateRequest = InferType<typeof merchantUpdateRequestBodySchema>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,14 @@ export type ProductUpdate = {
name: string;
image: string;
active: boolean;
mint: string;
};

export type TierUpdate = {
name: string;
threshold: number;
discount: number;
active: boolean;
mint: string;
id?: number;
name?: string;
threshold?: number;
discount?: number;
active?: boolean;
};

export class MerchantService {
Expand Down Expand Up @@ -142,23 +141,18 @@ export class MerchantService {
}

async upsertProducts(merchantId: string, products: ProductNode[]): Promise<Product[]> {
// 1. Fetch all existing products for the merchant.
const existingProducts = await this.prisma.product.findMany({
where: { merchantId: merchantId },
});

// 2. Create a Set of product IDs from the passed-in array.
const newProductIds = new Set(products.map(product => product.id));

// 3. Identify the products that exist in the database but not in the passed-in array.
const productsToDelete = existingProducts.filter(product => !newProductIds.has(product.id));

// 4. Create delete actions for the products to be deleted.
const deleteActions = productsToDelete.map(product =>
this.prisma.product.delete({ where: { id: product.id } })
);

// 5. Create upsert actions for the passed-in products.
const upsertActions = products.map(product =>
this.prisma.product.upsert({
where: { id: product.id },
Expand All @@ -175,47 +169,41 @@ export class MerchantService {
})
);

// 6. Perform the delete and upsert actions in a transaction.
const transactionResults = await this.prisma.$transaction([...deleteActions, ...upsertActions]);

// 7. Filter out the results of the delete actions to return only the upserted products.
const upsertedProducts = transactionResults.slice(deleteActions.length) as Product[];

return upsertedProducts;
}

async addTier(merchantId: string, tier: TierUpdate): Promise<Tier> {
return prismaErrorHandler(
this.prisma.tier.create({
data: {
...tier,
merchantId: merchantId,
},
async toggleProduct(product: { productId?: string; active?: boolean }): Promise<Product> {
return await prismaErrorHandler(
this.prisma.product.update({
where: { id: product.productId },
data: { active: product.active },
})
);
}

async updateTier(tierId: number, update: Partial<TierUpdate>): Promise<Tier> {
const filteredUpdate = filterUndefinedFields(update);

return prismaErrorHandler(
this.prisma.tier.update({
where: {
id: tierId,
},
data: filteredUpdate,
})
);
}

async removeTier(tierId: number): Promise<void> {
await prismaErrorHandler(
this.prisma.tier.delete({
where: {
id: tierId,
},
})
);
async upsertTier(merchantId: string, tier: TierUpdate): Promise<Tier> {
const filteredUpdate = filterUndefinedFields(tier);
if (tier.id) {
return prismaErrorHandler(
this.prisma.tier.update({
where: { id: tier.id },
data: filteredUpdate,
})
);
} else {
return prismaErrorHandler(
this.prisma.tier.create({
data: {
...filteredUpdate,
merchantId: merchantId,
},
})
);
}
}

async getProducts(merchantId: string): Promise<Product[]> {
Expand Down
26 changes: 18 additions & 8 deletions apps/merchant-ui/src/components/ProductsCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useToast } from '@/components/ui/use-toast';
import * as RE from '@/lib/Result';
import { Product, useMerchantStore } from '@/stores/merchantStore';
import { Product, updateMerchant, useMerchantStore } from '@/stores/merchantStore';
import { Switch } from './ui/switch';

interface Props {
Expand All @@ -12,21 +12,31 @@ export function ProductsCard(props: Props) {
const { toast } = useToast();

const merchantInfo = useMerchantStore(state => state.merchantInfo);
const getMerchantInfo = useMerchantStore(state => state.getMerchantInfo);

const products =
RE.isOk(merchantInfo) && merchantInfo.data.loyalty.products ? merchantInfo.data.loyalty.products : [];

async function handleEnable(product: Product) {
async function handleToggle(product: Product) {
try {
await updateMerchant('product', {
productId: product.id,
active: !product.active,
});
await getMerchantInfo();

toast({
title: 'Successfully enabled NFTs',
title: `Successfully ${product.active ? 'deactivated' : 'activated'} NFTs`,
variant: 'constructive',
});
} catch (error) {
toast({
title: 'Error enabling NFTs',
variant: 'destructive',
});
if (error instanceof Error) {
toast({
title: `Error ${product.active ? 'deactivating' : 'activating'} NFTs`,
description: error.message,
variant: 'destructive',
});
}
}
}
return (
Expand All @@ -44,7 +54,7 @@ export function ProductsCard(props: Props) {
<TableCell>{product.image}</TableCell>
<TableCell>{product.name}</TableCell>
<TableCell>
<Switch checked={product.active} onCheckedChange={() => handleEnable(product)} />
<Switch checked={product.active} onCheckedChange={() => handleToggle(product)} />
</TableCell>
</TableRow>
))}
Expand Down
Loading

1 comment on commit fcae9dd

@vercel
Copy link

@vercel vercel bot commented on fcae9dd Jul 27, 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.