add interest

This commit is contained in:
2025-12-29 13:34:33 -03:00
parent b2abe2ef0b
commit 62ce241479
4 changed files with 125 additions and 23 deletions

View File

@@ -19,6 +19,7 @@ from pydantic import (
from api_gateway import JSONResponse from api_gateway import JSONResponse
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import ORDER_TABLE from config import ORDER_TABLE
from routes.enrollments.enroll import Enrollment
router = Router() router = Router()
dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client) dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
@@ -66,6 +67,7 @@ class Checkout(BaseModel):
address: Address address: Address
payment_method: Literal['PIX', 'CREDIT_CARD', 'BANK_SLIP', 'MANUAL'] payment_method: Literal['PIX', 'CREDIT_CARD', 'BANK_SLIP', 'MANUAL']
items: tuple[Item, ...] items: tuple[Item, ...]
enrollments: tuple[Enrollment, ...] | None = None
coupon: Coupon | None = None coupon: Coupon | None = None
user: User | None = None user: User | None = None
org_id: UUID4 | str | None = None org_id: UUID4 | str | None = None

View File

@@ -29,10 +29,13 @@ 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 { useWizard } from '@/components/wizard' import { useWizard } from '@/components/wizard'
import { isName } from '../_.$orgid.users.add/data' import { isName } from '../_.$orgid.users.add/data'
import type { PaymentMethod } from '@repo/ui/routes/orders/data' import type { PaymentMethod } from '@repo/ui/routes/orders/data'
import type { WizardState } from './route'
import { applyDiscount } from './discount'
const creditCard = z.object({ const creditCard = z.object({
holder_name: z holder_name: z
@@ -52,31 +55,32 @@ const creditCard = z.object({
cvv: z.string().min(3).max(4) cvv: z.string().min(3).max(4)
}) })
const formSchema = z.discriminatedUnion( const formSchema = z.discriminatedUnion('payment_method', [
'payment_method',
[
z.object({ z.object({
payment_method: z.literal('PIX') payment_method: z.literal('PIX')
}), }),
z.object({ z.object({
payment_method: z.literal('BANK_SLIP') payment_method: z.literal('BANK_SLIP')
}), }),
z.object({ z.object({
payment_method: z.literal('MANUAL') payment_method: z.literal('MANUAL')
}), }),
z.object({ z.object({
payment_method: z.literal('CREDIT_CARD'), payment_method: z.literal('CREDIT_CARD'),
credit_card: creditCard credit_card: creditCard,
installments: z.coerce.number().int().min(1).max(12)
}) })
], ])
{ error: 'Escolha uma forma de pagamento' }
)
type Schema = z.infer<typeof formSchema> type Schema = z.input<typeof formSchema>
export type CreditCard = z.infer<typeof creditCard> export type CreditCard = z.infer<typeof creditCard>
type PaymentProps = { type PaymentProps = {
state: WizardState
onSubmit: (value: any) => void | Promise<void> onSubmit: (value: any) => void | Promise<void>
payment_method?: PaymentMethod payment_method?: PaymentMethod
credit_card?: CreditCard credit_card?: CreditCard
@@ -84,6 +88,7 @@ type PaymentProps = {
export function Payment({ export function Payment({
onSubmit, onSubmit,
state,
payment_method: paymentMethodInit, payment_method: paymentMethodInit,
credit_card: creditCardInit = undefined credit_card: creditCardInit = undefined
}: PaymentProps) { }: PaymentProps) {
@@ -91,11 +96,23 @@ export function Payment({
const { control, handleSubmit } = useForm<Schema>({ const { control, handleSubmit } = useForm<Schema>({
defaultValues: { defaultValues: {
payment_method: paymentMethodInit, payment_method: paymentMethodInit,
installments: state?.installments ?? 1,
credit_card: creditCardInit credit_card: creditCardInit
}, },
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}) })
const paymentMethod = useWatch({ control, name: 'payment_method' }) const paymentMethod = useWatch({ control, name: 'payment_method' })
const subtotal = state.items.reduce(
(acc, { course, quantity }) =>
acc +
(course?.unit_price || 0) *
(Number.isFinite(quantity) && quantity > 0 ? quantity : 1),
0
)
const discount = state.coupon
? applyDiscount(subtotal, state.coupon.amount, state.coupon.type) * -1
: 0
const total = subtotal > 0 ? subtotal + discount : 0
const onSubmit_ = async (data: Schema) => { const onSubmit_ = async (data: Schema) => {
await onSubmit({ credit_card: undefined, ...data }) await onSubmit({ credit_card: undefined, ...data })
@@ -147,7 +164,7 @@ export function Payment({
/> />
{paymentMethod === 'CREDIT_CARD' ? ( {paymentMethod === 'CREDIT_CARD' ? (
<CreditCard control={control} /> <CreditCard control={control} total={total} />
) : null} ) : null}
<Separator /> <Separator />
@@ -171,7 +188,13 @@ export function Payment({
) )
} }
export function CreditCard({ control }: { control: Control<Schema> }) { export function CreditCard({
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)
@@ -325,9 +348,58 @@ export function CreditCard({ control }: { control: Control<Schema> }) {
)} )}
/> />
</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 =
calcInterest(total, installment) / installment
return (
<NativeSelectOption
key={installment}
value={installment}
>
{installment}x <Currency>{value}</Currency> com juros
</NativeSelectOption>
)
})}
</NativeSelect>
</Field>
)}
/>
</FieldSet> </FieldSet>
</FieldGroup> </FieldGroup>
</CardContent> </CardContent>
</Card> </Card>
) )
} }
export const calcInterest = (total: number, installment: number) => {
const rate2to6 = 0.055
const rate7to12 = 0.0608
const rate = installment >= 7 ? rate7to12 : rate2to6
return total * ((1 - 0.0382) / (1 - rate))
}

View File

@@ -57,6 +57,7 @@ import {
InputGroupButton, InputGroupButton,
InputGroupInput InputGroupInput
} from '@repo/ui/components/ui/input-group' } from '@repo/ui/components/ui/input-group'
import { calcInterest } from './payment'
type ReviewProps = { type ReviewProps = {
state: WizardState state: WizardState
@@ -79,6 +80,13 @@ export function Review({ state, onSubmit }: ReviewProps) {
? applyDiscount(subtotal, coupon.amount, coupon.type) * -1 ? applyDiscount(subtotal, coupon.amount, coupon.type) * -1
: 0 : 0
const total = subtotal > 0 ? subtotal + discount : 0 const total = subtotal > 0 ? subtotal + discount : 0
const installments = state.installments
const interest_amount =
state.payment_method === 'CREDIT_CARD' &&
typeof installments === 'number' &&
installments > 1
? calcInterest(total, installments) - total
: 0
return ( return (
<> <>
@@ -117,6 +125,8 @@ export function Review({ state, onSubmit }: ReviewProps) {
) )
})} })}
</TableBody> </TableBody>
{/* Summary */}
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TableCell className="text-right" colSpan={3}> <TableCell className="text-right" colSpan={3}>
@@ -126,6 +136,7 @@ export function Review({ state, onSubmit }: ReviewProps) {
<Currency>{subtotal}</Currency> <Currency>{subtotal}</Currency>
</TableCell> </TableCell>
</TableRow> </TableRow>
{/* Discount */}
<TableRow> <TableRow>
<TableCell className="text-right" colSpan={3}> <TableCell className="text-right" colSpan={3}>
Descontos Descontos
@@ -134,12 +145,26 @@ export function Review({ state, onSubmit }: ReviewProps) {
<Currency>{discount}</Currency> <Currency>{discount}</Currency>
</TableCell> </TableCell>
</TableRow> </TableRow>
{/* Interest */}
{interest_amount ? (
<TableRow>
<TableCell className="text-right" colSpan={3}>
Juros
</TableCell>
<TableCell>
<Currency>{interest_amount}</Currency>
</TableCell>
</TableRow>
) : (
<></>
)}
{/* Total */}
<TableRow> <TableRow>
<TableCell className="text-right" colSpan={3}> <TableCell className="text-right" colSpan={3}>
Total Total
</TableCell> </TableCell>
<TableCell> <TableCell>
<Currency>{total}</Currency> <Currency>{total + interest_amount}</Currency>
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableFooter> </TableFooter>

View File

@@ -43,6 +43,7 @@ export type WizardState = {
items: Item[] items: Item[]
enrollments: Enrollment[] enrollments: Enrollment[]
coupon?: Coupon coupon?: Coupon
installments?: number
payment_method?: PaymentMethod payment_method?: PaymentMethod
credit_card?: CreditCard credit_card?: CreditCard
} }
@@ -54,6 +55,7 @@ const emptyWizard: WizardState = {
enrollments: [], enrollments: [],
coupon: undefined, coupon: undefined,
payment_method: undefined, payment_method: undefined,
installments: undefined,
credit_card: undefined credit_card: undefined
} }
@@ -204,6 +206,7 @@ export default function Route({
{/* Payment */} {/* Payment */}
<WizardStep name="payment"> <WizardStep name="payment">
<Payment <Payment
state={state}
payment_method={state.payment_method} payment_method={state.payment_method}
credit_card={state.credit_card} credit_card={state.credit_card}
onSubmit={(data: any) => { onSubmit={(data: any) => {