add retries to order
This commit is contained in:
@@ -62,5 +62,13 @@ def payment_retries(
|
|||||||
},
|
},
|
||||||
cond_expr='attribute_not_exists(sk)',
|
cond_expr='attribute_not_exists(sk)',
|
||||||
)
|
)
|
||||||
|
transact.update(
|
||||||
|
key=KeyPair(order_id, 'TRANSACTION#STATS'),
|
||||||
|
update_expr='SET updated_at = :now REMOVE last_attempt_succeeded',
|
||||||
|
expr_attr_values={
|
||||||
|
':now': now(),
|
||||||
|
},
|
||||||
|
cond_expr='attribute_exists(sk)',
|
||||||
|
)
|
||||||
|
|
||||||
return JSONResponse(status_code=HTTPStatus.CREATED)
|
return JSONResponse(status_code=HTTPStatus.CREATED)
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ def test_payment_retries(
|
|||||||
)
|
)
|
||||||
assert r['statusCode'] == HTTPStatus.CREATED
|
assert r['statusCode'] == HTTPStatus.CREATED
|
||||||
|
|
||||||
r = dynamodb_persistence_layer.collection.get_item(
|
r = dynamodb_persistence_layer.collection.get_items(
|
||||||
KeyPair('4b23f6f5-5377-476b-b1de-79427c0295f6', 'TRANSACTION')
|
KeyPair('4b23f6f5-5377-476b-b1de-79427c0295f6', 'TRANSACTION')
|
||||||
|
+ KeyPair(
|
||||||
|
'4b23f6f5-5377-476b-b1de-79427c0295f6',
|
||||||
|
'TRANSACTION#STATS',
|
||||||
|
rename_key='stats',
|
||||||
|
)
|
||||||
)
|
)
|
||||||
assert r['credit_card']['number'] == '4111111111111111'
|
assert r['credit_card']['number'] == '4111111111111111'
|
||||||
|
assert 'last_attempt_succeeded' not in r['stats']
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
// file: tests/routes/orders/test_payment_retries.py
|
// file: tests/routes/orders/test_payment_retries.py
|
||||||
{"id": "4b23f6f5-5377-476b-b1de-79427c0295f6", "sk": "0", "installments": 3}
|
{"id": "4b23f6f5-5377-476b-b1de-79427c0295f6", "sk": "0", "installments": 3}
|
||||||
{"id": "4b23f6f5-5377-476b-b1de-79427c0295f6", "sk": "INVOICE", "invoice_id": "123"}
|
{"id": "4b23f6f5-5377-476b-b1de-79427c0295f6", "sk": "INVOICE", "invoice_id": "123"}
|
||||||
|
{"id": "4b23f6f5-5377-476b-b1de-79427c0295f6", "sk": "TRANSACTION#STATS", "last_attempt_succeeded": false}
|
||||||
|
|
||||||
// Indicies
|
// Indicies
|
||||||
// CNPJs
|
// CNPJs
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
import { useForm, Controller, useWatch, type Control } from 'react-hook-form'
|
|
||||||
import { PatternFormat } from 'react-number-format'
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
|
||||||
import { ErrorMessage } from '@hookform/error-message'
|
import { ErrorMessage } from '@hookform/error-message'
|
||||||
import { z } from 'zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { ArrowRightIcon, CircleQuestionMarkIcon } from 'lucide-react'
|
|
||||||
import valid from 'card-validator'
|
import valid from 'card-validator'
|
||||||
|
import { ArrowRightIcon, CircleQuestionMarkIcon } from 'lucide-react'
|
||||||
|
import { Controller, useForm, useWatch, type Control } from 'react-hook-form'
|
||||||
|
import { PatternFormat } from 'react-number-format'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import { Currency } from '@repo/ui/components/currency'
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
|
||||||
import { Label } from '@repo/ui/components/ui/label'
|
|
||||||
import { RadioGroup, RadioGroupItem } from '@repo/ui/components/ui/radio-group'
|
|
||||||
import { Separator } from '@repo/ui/components/ui/separator'
|
|
||||||
import { Input } from '@repo/ui/components/ui/input'
|
|
||||||
import { Card, CardContent } from '@repo/ui/components/ui/card'
|
import { Card, CardContent } from '@repo/ui/components/ui/card'
|
||||||
import {
|
|
||||||
HoverCard,
|
|
||||||
HoverCardContent,
|
|
||||||
HoverCardTrigger
|
|
||||||
} from '@repo/ui/components/ui/hover-card'
|
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldError,
|
FieldError,
|
||||||
@@ -25,18 +16,27 @@ import {
|
|||||||
FieldLabel,
|
FieldLabel,
|
||||||
FieldSet
|
FieldSet
|
||||||
} from '@repo/ui/components/ui/field'
|
} from '@repo/ui/components/ui/field'
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger
|
||||||
|
} from '@repo/ui/components/ui/hover-card'
|
||||||
|
import { Input } from '@repo/ui/components/ui/input'
|
||||||
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||||
|
import { Label } from '@repo/ui/components/ui/label'
|
||||||
import {
|
import {
|
||||||
NativeSelect,
|
NativeSelect,
|
||||||
NativeSelectOption
|
NativeSelectOption
|
||||||
} from '@repo/ui/components/ui/native-select'
|
} from '@repo/ui/components/ui/native-select'
|
||||||
import { Currency } from '@repo/ui/components/currency'
|
import { RadioGroup, RadioGroupItem } from '@repo/ui/components/ui/radio-group'
|
||||||
|
import { Separator } from '@repo/ui/components/ui/separator'
|
||||||
|
|
||||||
import { useWizard } from '@/components/wizard'
|
import { useWizard } from '@/components/wizard'
|
||||||
import { isName } from '../_.$orgid.users.add/data'
|
import { isName } from '../_.$orgid.users.add/data'
|
||||||
import { applyDiscount } from './discount'
|
|
||||||
import { useWizardStore } from './store'
|
import { useWizardStore } from './store'
|
||||||
|
|
||||||
const creditCard = z.object({
|
export const creditCardSchema = z.object({
|
||||||
holder_name: z
|
holder_name: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -71,7 +71,7 @@ const formSchema = z.discriminatedUnion(
|
|||||||
|
|
||||||
z.object({
|
z.object({
|
||||||
payment_method: z.literal('CREDIT_CARD'),
|
payment_method: z.literal('CREDIT_CARD'),
|
||||||
credit_card: creditCard,
|
credit_card: creditCardSchema,
|
||||||
installments: z.coerce.number().int().min(1).max(12)
|
installments: z.coerce.number().int().min(1).max(12)
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
@@ -80,7 +80,7 @@ const formSchema = z.discriminatedUnion(
|
|||||||
|
|
||||||
type Schema = z.input<typeof formSchema>
|
type Schema = z.input<typeof formSchema>
|
||||||
|
|
||||||
export type CreditCard = z.infer<typeof creditCard>
|
export type CreditCard = z.infer<typeof creditCardSchema>
|
||||||
|
|
||||||
export function Payment({}) {
|
export function Payment({}) {
|
||||||
const wizard = useWizard()
|
const wizard = useWizard()
|
||||||
@@ -95,6 +95,7 @@ export function Payment({}) {
|
|||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(formSchema)
|
||||||
})
|
})
|
||||||
const paymentMethod = useWatch({ control, name: 'payment_method' })
|
const paymentMethod = useWatch({ control, name: 'payment_method' })
|
||||||
|
const total = subtotal + discount
|
||||||
|
|
||||||
const onSubmit = async ({ payment_method, ...data }: Schema) => {
|
const onSubmit = async ({ payment_method, ...data }: Schema) => {
|
||||||
if (payment_method === 'CREDIT_CARD') {
|
if (payment_method === 'CREDIT_CARD') {
|
||||||
@@ -156,7 +157,54 @@ export function Payment({}) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{paymentMethod === 'CREDIT_CARD' ? (
|
{paymentMethod === 'CREDIT_CARD' ? (
|
||||||
<CreditCard control={control} total={subtotal + discount} />
|
<Card className="lg:w-1/2">
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<CreditCard control={control} />
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="installments"
|
||||||
|
defaultValue={1}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<Field data-invalid={fieldState.invalid}>
|
||||||
|
<FieldLabel htmlFor={field.name}>Parcelas</FieldLabel>
|
||||||
|
<NativeSelect
|
||||||
|
id={field.name}
|
||||||
|
value={String(field.value)}
|
||||||
|
onChange={field.onChange}
|
||||||
|
aria-invalid={fieldState.invalid}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 12 }, (_, index) => {
|
||||||
|
const installment = index + 1 // 1 -> 12
|
||||||
|
|
||||||
|
if (installment === 1) {
|
||||||
|
return (
|
||||||
|
<NativeSelectOption key={installment} value={1}>
|
||||||
|
<Currency>{total}</Currency> à vista
|
||||||
|
</NativeSelectOption>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const value =
|
||||||
|
installment > 1
|
||||||
|
? calcInterest(total, installment) / installment
|
||||||
|
: total
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NativeSelectOption
|
||||||
|
key={installment}
|
||||||
|
value={installment}
|
||||||
|
>
|
||||||
|
{installment}x <Currency>{value}</Currency> com juros
|
||||||
|
</NativeSelectOption>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</NativeSelect>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -180,19 +228,11 @@ export function Payment({}) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CreditCard({
|
export function CreditCard({ control }: { control: Control<Schema> }) {
|
||||||
total,
|
|
||||||
control
|
|
||||||
}: {
|
|
||||||
total: number
|
|
||||||
control: Control<Schema>
|
|
||||||
}) {
|
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
const years = Array.from({ length: 10 }, (_, i) => currentYear + i)
|
const years = Array.from({ length: 10 }, (_, i) => currentYear + i)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="lg:w-1/2">
|
|
||||||
<CardContent>
|
|
||||||
<FieldGroup>
|
<FieldGroup>
|
||||||
<FieldSet>
|
<FieldSet>
|
||||||
{/* Credir card number */}
|
{/* Credir card number */}
|
||||||
@@ -217,9 +257,7 @@ export function CreditCard({
|
|||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{fieldState.invalid && (
|
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||||
<FieldError errors={[fieldState.error]} />
|
|
||||||
)}
|
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -236,9 +274,7 @@ export function CreditCard({
|
|||||||
aria-invalid={fieldState.invalid}
|
aria-invalid={fieldState.invalid}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
{fieldState.invalid && (
|
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||||
<FieldError errors={[fieldState.error]} />
|
|
||||||
)}
|
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -320,14 +356,14 @@ export function CreditCard({
|
|||||||
className="text-sm space-y-1.5 lg:w-78"
|
className="text-sm space-y-1.5 lg:w-78"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
O <Kbd>CVC</Kbd> é o código de segurança do cartão
|
O <Kbd>CVC</Kbd> é o código de segurança do cartão de
|
||||||
de crédito.
|
crédito.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Ele fica no verso do cartão e geralmente possui{' '}
|
Ele fica no verso do cartão e geralmente possui{' '}
|
||||||
<Kbd>3 dígitos</Kbd> (ou <Kbd>4 dígitos</Kbd> na
|
<Kbd>3 dígitos</Kbd> (ou <Kbd>4 dígitos</Kbd> na frente,
|
||||||
frente, no caso do American Express).
|
no caso do American Express).
|
||||||
</p>
|
</p>
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
@@ -341,53 +377,8 @@ export function CreditCard({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="installments"
|
|
||||||
defaultValue={1}
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<Field data-invalid={fieldState.invalid}>
|
|
||||||
<FieldLabel htmlFor={field.name}>Parcelas</FieldLabel>
|
|
||||||
<NativeSelect
|
|
||||||
id={field.name}
|
|
||||||
value={String(field.value)}
|
|
||||||
onChange={field.onChange}
|
|
||||||
aria-invalid={fieldState.invalid}
|
|
||||||
>
|
|
||||||
{Array.from({ length: 12 }, (_, index) => {
|
|
||||||
const installment = index + 1 // 1 -> 12
|
|
||||||
|
|
||||||
if (installment === 1) {
|
|
||||||
return (
|
|
||||||
<NativeSelectOption key={installment} value={1}>
|
|
||||||
<Currency>{total}</Currency> à vista
|
|
||||||
</NativeSelectOption>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const value =
|
|
||||||
installment > 1
|
|
||||||
? calcInterest(total, installment) / installment
|
|
||||||
: total
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NativeSelectOption
|
|
||||||
key={installment}
|
|
||||||
value={installment}
|
|
||||||
>
|
|
||||||
{installment}x <Currency>{value}</Currency> com juros
|
|
||||||
</NativeSelectOption>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</NativeSelect>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,34 @@
|
|||||||
|
import { formatCEP } from '@brazilian-utils/brazilian-utils'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { useRequest, useToggle } from 'ahooks'
|
import { useRequest, useToggle } from 'ahooks'
|
||||||
import { PatternFormat } from 'react-number-format'
|
import valid from 'card-validator'
|
||||||
import { ExternalLinkIcon, PencilIcon, SearchIcon } from 'lucide-react'
|
import { ExternalLinkIcon, PencilIcon, SearchIcon } from 'lucide-react'
|
||||||
import { Controller, useForm } from 'react-hook-form'
|
import { Controller, useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { PatternFormat } from 'react-number-format'
|
||||||
import { formatCEP } from '@brazilian-utils/brazilian-utils'
|
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import valid from 'card-validator'
|
|
||||||
|
|
||||||
import { Currency } from '@repo/ui/components/currency'
|
|
||||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
|
||||||
import { Abbr } from '@repo/ui/components/abbr'
|
import { Abbr } from '@repo/ui/components/abbr'
|
||||||
|
import { Currency } from '@repo/ui/components/currency'
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from '@repo/ui/components/ui/dialog'
|
||||||
|
import {
|
||||||
|
Item,
|
||||||
|
ItemActions,
|
||||||
|
ItemContent,
|
||||||
|
ItemDescription,
|
||||||
|
ItemGroup,
|
||||||
|
ItemTitle
|
||||||
|
} from '@repo/ui/components/ui/item'
|
||||||
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||||
import { Separator } from '@repo/ui/components/ui/separator'
|
import { Separator } from '@repo/ui/components/ui/separator'
|
||||||
import { Spinner } from '@repo/ui/components/ui/spinner'
|
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||||
import {
|
import {
|
||||||
@@ -22,24 +40,6 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow
|
TableRow
|
||||||
} from '@repo/ui/components/ui/table'
|
} from '@repo/ui/components/ui/table'
|
||||||
import {
|
|
||||||
Item,
|
|
||||||
ItemActions,
|
|
||||||
ItemContent,
|
|
||||||
ItemDescription,
|
|
||||||
ItemGroup,
|
|
||||||
ItemTitle
|
|
||||||
} from '@repo/ui/components/ui/item'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger
|
|
||||||
} from '@repo/ui/components/ui/dialog'
|
|
||||||
import { paymentMethods } from '@repo/ui/routes/orders/data'
|
import { paymentMethods } from '@repo/ui/routes/orders/data'
|
||||||
|
|
||||||
import { useWizard } from '@/components/wizard'
|
import { useWizard } from '@/components/wizard'
|
||||||
@@ -59,8 +59,6 @@ import {
|
|||||||
InputGroupInput
|
InputGroupInput
|
||||||
} from '@repo/ui/components/ui/input-group'
|
} from '@repo/ui/components/ui/input-group'
|
||||||
|
|
||||||
import { useWizardStore } from './store'
|
|
||||||
import { useParams } from 'react-router'
|
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
@@ -68,6 +66,8 @@ import {
|
|||||||
EmptyHeader,
|
EmptyHeader,
|
||||||
EmptyTitle
|
EmptyTitle
|
||||||
} from '@repo/ui/components/ui/empty'
|
} from '@repo/ui/components/ui/empty'
|
||||||
|
import { useParams } from 'react-router'
|
||||||
|
import { useWizardStore } from './store'
|
||||||
|
|
||||||
type ReviewProps = {
|
type ReviewProps = {
|
||||||
onSubmit: () => void | Promise<void>
|
onSubmit: () => void | Promise<void>
|
||||||
@@ -445,7 +445,7 @@ export function AddressDialog({ children }) {
|
|||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="text-black dark:text-white"
|
className="text-black dark:text-white"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { persist } from 'zustand/middleware'
|
import { persist } from 'zustand/middleware'
|
||||||
import { applyDiscount, type Coupon } from './discount'
|
|
||||||
import { calcInterest, type CreditCard } from './payment'
|
|
||||||
import type { PaymentMethod } from '@repo/ui/routes/orders/data'
|
import type { PaymentMethod } from '@repo/ui/routes/orders/data'
|
||||||
|
|
||||||
import type { Enrollment } from '../_.$orgid.enrollments.add/data'
|
import type { Enrollment } from '../_.$orgid.enrollments.add/data'
|
||||||
import type { Item } from './bulk'
|
import type { Item } from './bulk'
|
||||||
|
import { applyDiscount, type Coupon } from './discount'
|
||||||
|
import { calcInterest, type CreditCard } from './payment'
|
||||||
import type { Address } from './review'
|
import type { Address } from './review'
|
||||||
|
|
||||||
export type WizardState = {
|
export type WizardState = {
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import type { Route } from './+types/route'
|
import type { Route } from './+types/route'
|
||||||
|
|
||||||
import { formatCEP } from '@brazilian-utils/brazilian-utils'
|
import { formatCEP } from '@brazilian-utils/brazilian-utils'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { useRequest } from 'ahooks'
|
||||||
import {
|
import {
|
||||||
AlertCircleIcon,
|
AlertCircleIcon,
|
||||||
ArrowLeftRightIcon,
|
ArrowLeftRightIcon,
|
||||||
HelpCircleIcon
|
HelpCircleIcon
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
import { Link } from 'react-router'
|
import { Link } from 'react-router'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
import { Currency } from '@repo/ui/components/currency'
|
import { Currency } from '@repo/ui/components/currency'
|
||||||
import {
|
import {
|
||||||
@@ -52,7 +56,23 @@ import {
|
|||||||
type Order as Order_
|
type Order as Order_
|
||||||
} from '@repo/ui/routes/orders/data'
|
} from '@repo/ui/routes/orders/data'
|
||||||
|
|
||||||
import type { CreditCard as CreditCard_ } from '../_.$orgid.enrollments.buy/payment'
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from '@repo/ui/components/ui/dialog'
|
||||||
|
import { Separator } from '@repo/ui/components/ui/separator'
|
||||||
|
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||||
|
import {
|
||||||
|
CreditCard,
|
||||||
|
creditCardSchema,
|
||||||
|
type CreditCard as CreditCardProps
|
||||||
|
} from '../_.$orgid.enrollments.buy/payment'
|
||||||
import type { Address } from '../_.$orgid.enrollments.buy/review'
|
import type { Address } from '../_.$orgid.enrollments.buy/review'
|
||||||
import { useWizardStore } from '../_.$orgid.enrollments.buy/store'
|
import { useWizardStore } from '../_.$orgid.enrollments.buy/store'
|
||||||
|
|
||||||
@@ -65,12 +85,12 @@ export function meta() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PaymentMethodComponent = {
|
const PaymentMethodComponent = {
|
||||||
PIX: Pix,
|
PIX: PixPaymentMethod,
|
||||||
BANK_SLIP: BankSlip,
|
BANK_SLIP: BankSlipPaymentMethod,
|
||||||
CREDIT_CARD: CreditCard
|
CREDIT_CARD: CreditCardPaymentMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
type Item_ = {
|
type Item = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
unit_price: number
|
unit_price: number
|
||||||
@@ -82,18 +102,24 @@ type User = {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Invoice = {
|
||||||
|
invoice_id: string
|
||||||
|
secure_url: string
|
||||||
|
}
|
||||||
|
|
||||||
type Order = Order_ & {
|
type Order = Order_ & {
|
||||||
items: Item_[]
|
items: Item[]
|
||||||
interest_amount: number
|
interest_amount: number
|
||||||
due_date: string
|
due_date: string
|
||||||
created_at: string
|
created_at: string
|
||||||
subtotal: number
|
subtotal: number
|
||||||
discount: number
|
discount: number
|
||||||
address: Address
|
address: Address
|
||||||
credit_card?: CreditCard_
|
credit_card?: CreditCardProps
|
||||||
coupon?: string
|
coupon?: string
|
||||||
installments?: number
|
installments?: number
|
||||||
created_by?: User
|
created_by?: User
|
||||||
|
invoice: Invoice
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loader({ context, request, params }: Route.LoaderArgs) {
|
export async function loader({ context, request, params }: Route.LoaderArgs) {
|
||||||
@@ -115,11 +141,15 @@ export default function Route({ loaderData: { order } }: Route.ComponentProps) {
|
|||||||
payment_method,
|
payment_method,
|
||||||
interest_amount,
|
interest_amount,
|
||||||
discount,
|
discount,
|
||||||
|
invoice,
|
||||||
subtotal,
|
subtotal,
|
||||||
items = []
|
items = []
|
||||||
} = order
|
} = order
|
||||||
|
|
||||||
const Component = PaymentMethodComponent[payment_method]
|
const Component =
|
||||||
|
(PaymentMethodComponent as Record<string, React.ComponentType<any>>)[
|
||||||
|
payment_method
|
||||||
|
] ?? UnknownPaymentMethod
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset()
|
reset()
|
||||||
@@ -168,7 +198,9 @@ export default function Route({ loaderData: { order } }: Route.ComponentProps) {
|
|||||||
<ItemContent>
|
<ItemContent>
|
||||||
<ItemTitle>Forma de pagamento</ItemTitle>
|
<ItemTitle>Forma de pagamento</ItemTitle>
|
||||||
<div className="text-muted-foreground text-sm leading-normal font-normal text-balance">
|
<div className="text-muted-foreground text-sm leading-normal font-normal text-balance">
|
||||||
{Component && <Component {...order} />}
|
{Component && (
|
||||||
|
<Component {...order} invoice_id={invoice['invoice_id']} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ItemContent>
|
</ItemContent>
|
||||||
</Item>
|
</Item>
|
||||||
@@ -271,23 +303,50 @@ function Status({ status: s }: { status: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PaymentMethodProps = {
|
type PaymentMethodProps = {
|
||||||
|
id: string
|
||||||
status: string
|
status: string
|
||||||
total: number
|
total: number
|
||||||
|
invoice_id: string
|
||||||
installments: number
|
installments: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreditCardProps = PaymentMethodProps & {
|
type BankSlipPaymentMethodProps = PaymentMethodProps & {}
|
||||||
|
|
||||||
|
function BankSlipPaymentMethod({ status }: BankSlipPaymentMethodProps) {
|
||||||
|
return (
|
||||||
|
<ul className="flex max-lg:flex-col gap-x-1.5">
|
||||||
|
<li>Boleto bancário</li>
|
||||||
|
<li>
|
||||||
|
<Status status={status} />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PixPaymentMethodrops = PaymentMethodProps & {}
|
||||||
|
|
||||||
|
function PixPaymentMethod({}: PixPaymentMethodrops) {
|
||||||
|
return <>Pix</>
|
||||||
|
}
|
||||||
|
|
||||||
|
function UnknownPaymentMethod() {
|
||||||
|
return <>Deposito bancário</>
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreditCardPaymentMethodProps = PaymentMethodProps & {
|
||||||
stats: { last_attempt_succeeded: boolean }
|
stats: { last_attempt_succeeded: boolean }
|
||||||
credit_card: { last4: string; brand: string }
|
credit_card: { last4: string; brand: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreditCard({
|
function CreditCardPaymentMethod({
|
||||||
|
id,
|
||||||
status,
|
status,
|
||||||
total,
|
total,
|
||||||
credit_card,
|
credit_card,
|
||||||
installments,
|
installments,
|
||||||
|
invoice_id,
|
||||||
stats
|
stats
|
||||||
}: CreditCardProps) {
|
}: CreditCardPaymentMethodProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ul className="flex max-lg:flex-col gap-x-1.5">
|
<ul className="flex max-lg:flex-col gap-x-1.5">
|
||||||
@@ -295,12 +354,12 @@ function CreditCard({
|
|||||||
{credit_card.brand} (Crédito) **** {credit_card.last4}
|
{credit_card.brand} (Crédito) **** {credit_card.last4}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{!stats.last_attempt_succeeded ? (
|
{stats.last_attempt_succeeded === false ? (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-red-400 border-red-400 px-1.5"
|
className="text-red-400 border-red-400 px-1.5"
|
||||||
>
|
>
|
||||||
<AlertCircleIcon /> Transação negada
|
<AlertCircleIcon /> Pagamento não aprovado
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Status status={status} />
|
<Status status={status} />
|
||||||
@@ -312,32 +371,94 @@ function CreditCard({
|
|||||||
{installments}x <Currency>{total / Number(installments)}</Currency>
|
{installments}x <Currency>{total / Number(installments)}</Currency>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{!stats.last_attempt_succeeded ? (
|
{stats.last_attempt_succeeded === false ? (
|
||||||
<div className="flex justify-center mt-2">
|
<div className="flex justify-center mt-2">
|
||||||
|
<CreditCardPaymentDialog
|
||||||
|
id={id}
|
||||||
|
total={total}
|
||||||
|
installments={installments}
|
||||||
|
invoice_id={invoice_id}
|
||||||
|
>
|
||||||
<Button size="sm" variant="secondary" className="cursor-pointer">
|
<Button size="sm" variant="secondary" className="cursor-pointer">
|
||||||
<ArrowLeftRightIcon /> Tentar com outro cartão
|
<ArrowLeftRightIcon /> Pagar com outro cartão
|
||||||
</Button>
|
</Button>
|
||||||
|
</CreditCardPaymentDialog>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type BankSlipProps = PaymentMethodProps & {}
|
const formSchema = z.object({
|
||||||
|
credit_card: creditCardSchema
|
||||||
|
})
|
||||||
|
|
||||||
|
type Schema = z.input<typeof formSchema>
|
||||||
|
|
||||||
|
function CreditCardPaymentDialog({
|
||||||
|
children,
|
||||||
|
id,
|
||||||
|
installments,
|
||||||
|
invoice_id,
|
||||||
|
total
|
||||||
|
}) {
|
||||||
|
const { runAsync } = useRequest(
|
||||||
|
async ({ credit_card }) => {
|
||||||
|
return await fetch(`/~/api/orders/${id}/payment-retries`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||||
|
body: JSON.stringify({ credit_card, installments, invoice_id })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ manual: true }
|
||||||
|
)
|
||||||
|
const { control, handleSubmit, formState } = useForm<Schema>({
|
||||||
|
resolver: zodResolver(formSchema)
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = async ({ credit_card }: Schema) => {
|
||||||
|
const r = await runAsync({ credit_card })
|
||||||
|
console.log(r.ok)
|
||||||
|
console.log(await r.json())
|
||||||
|
}
|
||||||
|
|
||||||
function BankSlip({ status }: BankSlipProps) {
|
|
||||||
return (
|
return (
|
||||||
<ul className="flex max-lg:flex-col gap-x-1.5">
|
<Dialog>
|
||||||
<li>Boleto bancário</li>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<li>
|
|
||||||
<Status status={status} />
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
</li>
|
<DialogHeader>
|
||||||
</ul>
|
<DialogTitle>Novo cartão de crédito</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Use um novo cartão para concluir o pagamento. Nenhuma cobrança foi
|
||||||
|
realizada no cartão anterior.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<CreditCard control={control} />
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<DialogFooter className="*:cursor-pointer">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="link"
|
||||||
|
className="text-black dark:text-white"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
|
||||||
|
<Button type="submit" disabled={formState.isSubmitting}>
|
||||||
|
{formState.isSubmitting && <Spinner />}
|
||||||
|
Pagar <Currency>{total}</Currency>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PixProps = PaymentMethodProps & {}
|
|
||||||
|
|
||||||
function Pix({}: PixProps) {
|
|
||||||
return <>Pix</>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
'id': order_id,
|
'id': order_id,
|
||||||
'sk': 'INVOICE',
|
'sk': 'INVOICE',
|
||||||
'payment_method': payment_method,
|
'payment_method': payment_method,
|
||||||
'secure_id': invoice['secure_id'],
|
'invoice_id': invoice['secure_id'],
|
||||||
'secure_url': invoice['secure_url'],
|
'secure_url': invoice['secure_url'],
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
# Uncomment this when adding for multiple payment providers
|
# Uncomment this when adding for multiple payment providers
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ def test_create_bank_slip_invoice(
|
|||||||
invoice['secure_url']
|
invoice['secure_url']
|
||||||
== 'https://checkout.iugu.com/invoices/16f7aa3d-2e0b-41e9-987b-1dc95b957456-d7a2'
|
== 'https://checkout.iugu.com/invoices/16f7aa3d-2e0b-41e9-987b-1dc95b957456-d7a2'
|
||||||
)
|
)
|
||||||
assert invoice['secure_id'] == '16f7aa3d-2e0b-41e9-987b-1dc95b957456-d7a2'
|
assert invoice['invoice_id'] == '16f7aa3d-2e0b-41e9-987b-1dc95b957456-d7a2'
|
||||||
assert (
|
assert (
|
||||||
invoice['bank_slip']['bank_slip_url']
|
invoice['bank_slip']['bank_slip_url']
|
||||||
== 'https://boletos.iugu.com/v1/public/invoice/16f7aa3d-2e0b-41e9-987b-1dc95b957456-d7a2/bank_slip'
|
== 'https://boletos.iugu.com/v1/public/invoice/16f7aa3d-2e0b-41e9-987b-1dc95b957456-d7a2/bank_slip'
|
||||||
|
|||||||
Reference in New Issue
Block a user