From 4ce5be7a4c693a2822b5b8e7d2cb5af813af8e82 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Wed, 15 May 2024 23:40:59 +0700 Subject: [PATCH] feat: choose country before --- .../src/api/routes/marketplace.controller.ts | 7 +- .../src/components/marketplace/seller.tsx | 64 +++++++- .../src/services/stripe.country.list.ts | 111 ++++++++++++++ .../src/services/stripe.service.ts | 139 +++++++++++------- 4 files changed, 265 insertions(+), 56 deletions(-) create mode 100644 libraries/nestjs-libraries/src/services/stripe.country.list.ts diff --git a/apps/backend/src/api/routes/marketplace.controller.ts b/apps/backend/src/api/routes/marketplace.controller.ts index fa249726..a9d500e5 100644 --- a/apps/backend/src/api/routes/marketplace.controller.ts +++ b/apps/backend/src/api/routes/marketplace.controller.ts @@ -53,8 +53,11 @@ export class MarketplaceController { } @Get('/bank') - connectBankAccount(@GetUserFromRequest() user: User) { - return this._stripeService.createAccountProcess(user.id, user.email); + connectBankAccount( + @GetUserFromRequest() user: User, + @Query('country') country: string + ) { + return this._stripeService.createAccountProcess(user.id, user.email, country); } @Post('/item') diff --git a/apps/frontend/src/components/marketplace/seller.tsx b/apps/frontend/src/components/marketplace/seller.tsx index 0f9307de..df6b761d 100644 --- a/apps/frontend/src/components/marketplace/seller.tsx +++ b/apps/frontend/src/components/marketplace/seller.tsx @@ -4,12 +4,53 @@ import { Slider } from '@gitroom/react/form/slider'; import { Button } from '@gitroom/react/form/button'; import { tagsList } from '@gitroom/nestjs-libraries/database/prisma/marketplace/tags.list'; import { Options } from '@gitroom/frontend/components/marketplace/buyer'; -import { ChangeEvent, useCallback, useEffect, useState } from 'react'; +import { ChangeEvent, FC, useCallback, useEffect, useState } from 'react'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import useSWR from 'swr'; import { Input } from '@gitroom/react/form/input'; import { useDebouncedCallback } from 'use-debounce'; import { OrderList } from '@gitroom/frontend/components/marketplace/order.list'; +import { useModals } from '@mantine/modals'; +import { Select } from '@gitroom/react/form/select'; +import { countries } from '@gitroom/nestjs-libraries/services/stripe.country.list'; + +export const AddAccount: FC<{ openBankAccount: (country: string) => void }> = ( + props +) => { + const { openBankAccount } = props; + const [country, setCountry] = useState(''); + return ( +
+ Please select your country where your business is registered. +
+ + THIS IS IRREVERSIBLE. + + + +
+ ); +}; export const Seller = () => { const fetch = useFetch(); @@ -20,6 +61,7 @@ export const Seller = () => { const [connectedLoading, setConnectedLoading] = useState(false); const [state, setState] = useState(true); const [audience, setAudience] = useState(0); + const modals = useModals(); const accountInformation = useCallback(async () => { const account = await ( @@ -43,10 +85,10 @@ export const Seller = () => { }); }, []); - const connectBankAccount = useCallback(async () => { + const connectBankAccountLink = useCallback(async (country: string) => { setConnectedLoading(true); const { url } = await ( - await fetch('/marketplace/bank', { + await fetch(`/marketplace/bank?country=${country}`, { method: 'GET', }) ).json(); @@ -98,6 +140,22 @@ export const Seller = () => { const { data } = useSWR('/marketplace/account', accountInformation); + const connectBankAccount = useCallback(async () => { + if (!data?.account) { + modals.openModal({ + size: '100%', + classNames: { + modal: 'bg-transparent text-white', + }, + withCloseButton: false, + children: , + }); + return; + } + + connectBankAccountLink(''); + }, [data, connectBankAccountLink]); + useEffect(() => { loadItems(); }, []); diff --git a/libraries/nestjs-libraries/src/services/stripe.country.list.ts b/libraries/nestjs-libraries/src/services/stripe.country.list.ts new file mode 100644 index 00000000..8ced7d41 --- /dev/null +++ b/libraries/nestjs-libraries/src/services/stripe.country.list.ts @@ -0,0 +1,111 @@ +export const countries = [ + { value: 'AL', label: 'Albania' }, + { value: 'AG', label: 'Antigua & Barbuda' }, + { value: 'AR', label: 'Argentina' }, + { value: 'AM', label: 'Armenia' }, + { value: 'AU', label: 'Australia' }, + { value: 'AT', label: 'Austria' }, + { value: 'BS', label: 'Bahamas' }, + { value: 'BH', label: 'Bahrain' }, + { value: 'BE', label: 'Belgium' }, + { value: 'BJ', label: 'Benin' }, + { value: 'BO', label: 'Bolivia' }, + { value: 'BA', label: 'Bosnia & Herzegovina' }, + { value: 'BW', label: 'Botswana' }, + { value: 'BN', label: 'Brunei' }, + { value: 'BG', label: 'Bulgaria' }, + { value: 'KH', label: 'Cambodia' }, + { value: 'CA', label: 'Canada' }, + { value: 'CL', label: 'Chile' }, + { value: 'CO', label: 'Colombia' }, + { value: 'CR', label: 'Costa Rica' }, + { value: 'HR', label: 'Croatia' }, + { value: 'CY', label: 'Cyprus' }, + { value: 'CZ', label: 'Czech Republic' }, + { value: 'CI', label: 'Côte d’Ivoire' }, + { value: 'DK', label: 'Denmark' }, + { value: 'DO', label: 'Dominican Republic' }, + { value: 'EC', label: 'Ecuador' }, + { value: 'EG', label: 'Egypt' }, + { value: 'SV', label: 'El Salvador' }, + { value: 'EE', label: 'Estonia' }, + { value: 'ET', label: 'Ethiopia' }, + { value: 'FI', label: 'Finland' }, + { value: 'FR', label: 'France' }, + { value: 'GM', label: 'Gambia' }, + { value: 'DE', label: 'Germany' }, + { value: 'GH', label: 'Ghana' }, + { value: 'GI', label: 'Gibraltar' }, + { value: 'GR', label: 'Greece' }, + { value: 'GT', label: 'Guatemala' }, + { value: 'GY', label: 'Guyana' }, + { value: 'HK', label: 'Hong Kong SAR China' }, + { value: 'HU', label: 'Hungary' }, + { value: 'IS', label: 'Iceland' }, + { value: 'IN', label: 'India' }, + { value: 'ID', label: 'Indonesia' }, + { value: 'IE', label: 'Ireland' }, + { value: 'IL', label: 'Israel' }, + { value: 'IT', label: 'Italy' }, + { value: 'JM', label: 'Jamaica' }, + { value: 'JP', label: 'Japan' }, + { value: 'JO', label: 'Jordan' }, + { value: 'KE', label: 'Kenya' }, + { value: 'KW', label: 'Kuwait' }, + { value: 'LV', label: 'Latvia' }, + { value: 'LI', label: 'Liechtenstein' }, + { value: 'LT', label: 'Lithuania' }, + { value: 'LU', label: 'Luxembourg' }, + { value: 'MO', label: 'Macao SAR China' }, + { value: 'MG', label: 'Madagascar' }, + { value: 'MY', label: 'Malaysia' }, + { value: 'MT', label: 'Malta' }, + { value: 'MU', label: 'Mauritius' }, + { value: 'MX', label: 'Mexico' }, + { value: 'MD', label: 'Moldova' }, + { value: 'MC', label: 'Monaco' }, + { value: 'MN', label: 'Mongolia' }, + { value: 'MA', label: 'Morocco' }, + { value: 'NA', label: 'Namibia' }, + { value: 'NL', label: 'Netherlands' }, + { value: 'NZ', label: 'New Zealand' }, + { value: 'NG', label: 'Nigeria' }, + { value: 'MK', label: 'North Macedonia' }, + { value: 'NO', label: 'Norway' }, + { value: 'OM', label: 'Oman' }, + { value: 'PK', label: 'Pakistan' }, + { value: 'PA', label: 'Panama' }, + { value: 'PY', label: 'Paraguay' }, + { value: 'PE', label: 'Peru' }, + { value: 'PH', label: 'Philippines' }, + { value: 'PL', label: 'Poland' }, + { value: 'PT', label: 'Portugal' }, + { value: 'QA', label: 'Qatar' }, + { value: 'RO', label: 'Romania' }, + { value: 'RW', label: 'Rwanda' }, + { value: 'SA', label: 'Saudi Arabia' }, + { value: 'SN', label: 'Senegal' }, + { value: 'RS', label: 'Serbia' }, + { value: 'SG', label: 'Singapore' }, + { value: 'SK', label: 'Slovakia' }, + { value: 'SI', label: 'Slovenia' }, + { value: 'ZA', label: 'South Africa' }, + { value: 'KR', label: 'South Korea' }, + { value: 'ES', label: 'Spain' }, + { value: 'LK', label: 'Sri Lanka' }, + { value: 'LC', label: 'St. Lucia' }, + { value: 'SE', label: 'Sweden' }, + { value: 'CH', label: 'Switzerland' }, + { value: 'TW', label: 'Taiwan' }, + { value: 'TZ', label: 'Tanzania' }, + { value: 'TH', label: 'Thailand' }, + { value: 'TT', label: 'Trinidad & Tobago' }, + { value: 'TN', label: 'Tunisia' }, + { value: 'TR', label: 'Turkey' }, + { value: 'AE', label: 'United Arab Emirates' }, + { value: 'GB', label: 'United Kingdom' }, + { value: 'US', label: 'United States' }, + { value: 'UY', label: 'Uruguay' }, + { value: 'UZ', label: 'Uzbekistan' }, + { value: 'VN', label: 'Vietnam' }, +]; diff --git a/libraries/nestjs-libraries/src/services/stripe.service.ts b/libraries/nestjs-libraries/src/services/stripe.service.ts index 406766d1..588292f3 100644 --- a/libraries/nestjs-libraries/src/services/stripe.service.ts +++ b/libraries/nestjs-libraries/src/services/stripe.service.ts @@ -135,32 +135,44 @@ export class StripeService { expand: ['data.prices'], }); - const findProduct = allProducts.data.find( - (product) => product.name.toUpperCase() === body.billing.toUpperCase() - ) || await stripe.products.create({ - active: true, - name: body.billing, - }); + const findProduct = + allProducts.data.find( + (product) => product.name.toUpperCase() === body.billing.toUpperCase() + ) || + (await stripe.products.create({ + active: true, + name: body.billing, + })); const pricesList = await stripe.prices.list({ active: true, product: findProduct!.id, }); - const findPrice = pricesList.data.find( - (p) => - p?.recurring?.interval?.toLowerCase() === (body.period === 'MONTHLY' ? 'month' : 'year') && - p?.unit_amount === (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100 - ) || await stripe.prices.create({ - active: true, - product: findProduct!.id, - currency: 'usd', - nickname: body.billing + ' ' + body.period, - unit_amount: (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100, - recurring: { - interval: body.period === 'MONTHLY' ? 'month' : 'year', - }, - }); + const findPrice = + pricesList.data.find( + (p) => + p?.recurring?.interval?.toLowerCase() === + (body.period === 'MONTHLY' ? 'month' : 'year') && + p?.unit_amount === + (body.period === 'MONTHLY' + ? priceData.month_price + : priceData.year_price) * + 100 + ) || + (await stripe.prices.create({ + active: true, + product: findProduct!.id, + currency: 'usd', + nickname: body.billing + ' ' + body.period, + unit_amount: + (body.period === 'MONTHLY' + ? priceData.month_price + : priceData.year_price) * 100, + recurring: { + interval: body.period === 'MONTHLY' ? 'month' : 'year', + }, + })); const proration_date = Math.floor(Date.now() / 1000); @@ -269,29 +281,39 @@ export class StripeService { return { url }; } - async createAccountProcess(userId: string, email: string) { + async createAccountProcess(userId: string, email: string, country: string) { const account = (await this._subscriptionService.getUserAccount(userId))?.account || - (await this.createAccount(userId, email)); + (await this.createAccount(userId, email, country)); return { url: await this.addBankAccount(account) }; } - async createAccount(userId: string, email: string) { + async createAccount(userId: string, email: string, country: string) { const account = await stripe.accounts.create({ - controller: { - stripe_dashboard: { - type: 'express', + type: 'custom', + // controller: { + // stripe_dashboard: { + // type: 'express', + // }, + // fees: { + // payer: 'application', + // }, + // losses: { + // payments: 'application', + // }, + // }, + capabilities: { + transfers: { + requested: true, }, - fees: { - payer: 'application', - }, - losses: { - payments: 'application', + card_payments: { + requested: true, }, }, metadata: { service: 'gitroom', }, + country, email, }); @@ -306,6 +328,9 @@ export class StripeService { refresh_url: process.env['FRONTEND_URL'] + '/marketplace/seller', return_url: process.env['FRONTEND_URL'] + '/marketplace/seller', type: 'account_onboarding', + collection_options: { + fields: 'eventually_due', + }, }); return accountLink.url; @@ -378,32 +403,44 @@ export class StripeService { expand: ['data.prices'], }); - const findProduct = allProducts.data.find( - (product) => product.name.toUpperCase() === body.billing.toUpperCase() - ) || await stripe.products.create({ - active: true, - name: body.billing, - }); + const findProduct = + allProducts.data.find( + (product) => product.name.toUpperCase() === body.billing.toUpperCase() + ) || + (await stripe.products.create({ + active: true, + name: body.billing, + })); const pricesList = await stripe.prices.list({ active: true, product: findProduct!.id, }); - const findPrice = pricesList.data.find( - (p) => - p?.recurring?.interval?.toLowerCase() === (body.period === 'MONTHLY' ? 'month' : 'year') && - p?.unit_amount === (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100 - ) || await stripe.prices.create({ - active: true, - product: findProduct!.id, - currency: 'usd', - nickname: body.billing + ' ' + body.period, - unit_amount: (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100, - recurring: { - interval: body.period === 'MONTHLY' ? 'month' : 'year', - }, - }); + const findPrice = + pricesList.data.find( + (p) => + p?.recurring?.interval?.toLowerCase() === + (body.period === 'MONTHLY' ? 'month' : 'year') && + p?.unit_amount === + (body.period === 'MONTHLY' + ? priceData.month_price + : priceData.year_price) * + 100 + ) || + (await stripe.prices.create({ + active: true, + product: findProduct!.id, + currency: 'usd', + nickname: body.billing + ' ' + body.period, + unit_amount: + (body.period === 'MONTHLY' + ? priceData.month_price + : priceData.year_price) * 100, + recurring: { + interval: body.period === 'MONTHLY' ? 'month' : 'year', + }, + })); const currentUserSubscription = await stripe.subscriptions.list({ customer,