add payment retries
This commit is contained in:
@@ -50,6 +50,15 @@ def payment_retries(
|
|||||||
},
|
},
|
||||||
exc_cls=InvoiceNotFoundError,
|
exc_cls=InvoiceNotFoundError,
|
||||||
)
|
)
|
||||||
|
transact.put(
|
||||||
|
item={
|
||||||
|
'id': order_id,
|
||||||
|
'sk': 'CREDIT_CARD',
|
||||||
|
'brand': credit_card.brand,
|
||||||
|
'last4': credit_card.last4,
|
||||||
|
'created_at': now_,
|
||||||
|
}
|
||||||
|
)
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
from http import HTTPMethod, HTTPStatus
|
from http import HTTPMethod, HTTPStatus
|
||||||
|
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
from layercake.dynamodb import (
|
||||||
|
DynamoDBPersistenceLayer,
|
||||||
|
SortKey,
|
||||||
|
TransactKey,
|
||||||
|
)
|
||||||
|
|
||||||
from ...conftest import HttpApiProxy, LambdaContext
|
from ...conftest import HttpApiProxy, LambdaContext
|
||||||
|
|
||||||
@@ -33,12 +37,18 @@ def test_payment_retries(
|
|||||||
assert r['statusCode'] == HTTPStatus.CREATED
|
assert r['statusCode'] == HTTPStatus.CREATED
|
||||||
|
|
||||||
r = dynamodb_persistence_layer.collection.get_items(
|
r = dynamodb_persistence_layer.collection.get_items(
|
||||||
KeyPair('4b23f6f5-5377-476b-b1de-79427c0295f6', 'TRANSACTION')
|
TransactKey('4b23f6f5-5377-476b-b1de-79427c0295f6')
|
||||||
+ KeyPair(
|
+ SortKey('TRANSACTION')
|
||||||
'4b23f6f5-5377-476b-b1de-79427c0295f6',
|
+ SortKey(
|
||||||
|
'CREDIT_CARD',
|
||||||
|
rename_key='cc',
|
||||||
|
)
|
||||||
|
+ SortKey(
|
||||||
'TRANSACTION#STATS',
|
'TRANSACTION#STATS',
|
||||||
rename_key='stats',
|
rename_key='stats',
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
assert r['credit_card']['number'] == '4111111111111111'
|
assert r['credit_card']['number'] == '4111111111111111'
|
||||||
assert 'last_attempt_succeeded' not in r['stats']
|
assert 'last_attempt_succeeded' not in r['stats']
|
||||||
|
assert r['cc']['last4'] == '1111'
|
||||||
|
assert r['cc']['brand'] == 'Visa'
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import { useRequest } from 'ahooks'
|
|||||||
import {
|
import {
|
||||||
AlertCircleIcon,
|
AlertCircleIcon,
|
||||||
ArrowLeftRightIcon,
|
ArrowLeftRightIcon,
|
||||||
|
CircleCheckIcon,
|
||||||
|
CircleXIcon,
|
||||||
|
EllipsisIcon,
|
||||||
HelpCircleIcon
|
HelpCircleIcon
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
@@ -28,8 +31,16 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle
|
CardTitle
|
||||||
} from '@repo/ui/components/ui/card'
|
} from '@repo/ui/components/ui/card'
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandList
|
||||||
|
} from '@repo/ui/components/ui/command'
|
||||||
import {
|
import {
|
||||||
Item,
|
Item,
|
||||||
|
ItemActions,
|
||||||
ItemContent,
|
ItemContent,
|
||||||
ItemGroup,
|
ItemGroup,
|
||||||
ItemTitle
|
ItemTitle
|
||||||
@@ -46,16 +57,9 @@ import {
|
|||||||
import { request as req } from '@repo/util/request'
|
import { request as req } from '@repo/util/request'
|
||||||
|
|
||||||
import { Abbr } from '@repo/ui/components/abbr'
|
import { Abbr } from '@repo/ui/components/abbr'
|
||||||
|
import { DateTime } from '@repo/ui/components/datetime'
|
||||||
import { Badge } from '@repo/ui/components/ui/badge'
|
import { Badge } from '@repo/ui/components/ui/badge'
|
||||||
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 { cn } from '@repo/ui/lib/utils'
|
|
||||||
import {
|
|
||||||
labels,
|
|
||||||
statuses,
|
|
||||||
type Order as Order_
|
|
||||||
} from '@repo/ui/routes/orders/data'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
@@ -66,8 +70,20 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger
|
DialogTrigger
|
||||||
} from '@repo/ui/components/ui/dialog'
|
} from '@repo/ui/components/ui/dialog'
|
||||||
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger
|
||||||
|
} from '@repo/ui/components/ui/popover'
|
||||||
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 { cn } from '@repo/ui/lib/utils'
|
||||||
|
import {
|
||||||
|
labels,
|
||||||
|
statuses,
|
||||||
|
type Order as Order_
|
||||||
|
} from '@repo/ui/routes/orders/data'
|
||||||
import {
|
import {
|
||||||
CreditCard,
|
CreditCard,
|
||||||
creditCardSchema,
|
creditCardSchema,
|
||||||
@@ -107,6 +123,13 @@ type Invoice = {
|
|||||||
secure_url: string
|
secure_url: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Attempts = {
|
||||||
|
sk: string
|
||||||
|
status: string
|
||||||
|
brand: string
|
||||||
|
last4: string
|
||||||
|
}
|
||||||
|
|
||||||
type Order = Order_ & {
|
type Order = Order_ & {
|
||||||
items: Item[]
|
items: Item[]
|
||||||
interest_amount: number
|
interest_amount: number
|
||||||
@@ -115,6 +138,7 @@ type Order = Order_ & {
|
|||||||
subtotal: number
|
subtotal: number
|
||||||
discount: number
|
discount: number
|
||||||
address: Address
|
address: Address
|
||||||
|
payment_attempts: Attempts[]
|
||||||
credit_card?: CreditCardProps
|
credit_card?: CreditCardProps
|
||||||
coupon?: string
|
coupon?: string
|
||||||
installments?: number
|
installments?: number
|
||||||
@@ -142,8 +166,9 @@ export default function Route({ loaderData: { order } }: Route.ComponentProps) {
|
|||||||
interest_amount,
|
interest_amount,
|
||||||
discount,
|
discount,
|
||||||
invoice,
|
invoice,
|
||||||
subtotal,
|
payment_attempts = [],
|
||||||
items = []
|
items = [],
|
||||||
|
subtotal
|
||||||
} = order
|
} = order
|
||||||
|
|
||||||
const Component =
|
const Component =
|
||||||
@@ -203,6 +228,12 @@ export default function Route({ loaderData: { order } }: Route.ComponentProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ItemContent>
|
</ItemContent>
|
||||||
|
|
||||||
|
{payment_attempts.length > 0 ? (
|
||||||
|
<ItemActions>
|
||||||
|
<PaymentAttemptsMenu payment_attempts={payment_attempts} />
|
||||||
|
</ItemActions>
|
||||||
|
) : null}
|
||||||
</Item>
|
</Item>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@@ -462,3 +493,60 @@ function CreditCardPaymentDialog({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PaymentAttemptsMenu({
|
||||||
|
payment_attempts
|
||||||
|
}: {
|
||||||
|
payment_attempts: Attempts[]
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="ghost" className="cursor-pointer">
|
||||||
|
<EllipsisIcon />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align="end" className="w-76">
|
||||||
|
{/* <div className="border-b p-2 text-xs text-muted-foreground font-medium">
|
||||||
|
Transações
|
||||||
|
</div>*/}
|
||||||
|
<div className="p-2 space-y-1.5">
|
||||||
|
{payment_attempts.map(
|
||||||
|
({ sk, brand, last4, status, ...props }, index) => {
|
||||||
|
const [, , created_at] = sk.split('#')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul key={index} className="text-sm flex gap-1.5">
|
||||||
|
<li>
|
||||||
|
<Kbd>
|
||||||
|
<DateTime
|
||||||
|
options={{
|
||||||
|
year: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{created_at}
|
||||||
|
</DateTime>
|
||||||
|
</Kbd>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Abbr maxLen={6}>{brand}</Abbr>
|
||||||
|
</li>
|
||||||
|
<li className="ml-auto">**** {last4}</li>
|
||||||
|
<li className="flex items-center">
|
||||||
|
{status === 'FAILED' ? (
|
||||||
|
<CircleXIcon className="size-4 text-red-500" />
|
||||||
|
) : (
|
||||||
|
<CircleCheckIcon className="size-4 text-green-500" />
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import type { Route } from './+types/route'
|
|||||||
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
||||||
import { data } from 'react-router'
|
import { data } from 'react-router'
|
||||||
|
|
||||||
import { createSearch } from '@repo/util/meili'
|
|
||||||
import { cloudflareContext } from '@repo/auth/context'
|
import { cloudflareContext } from '@repo/auth/context'
|
||||||
|
import { createSearch } from '@repo/util/meili'
|
||||||
|
|
||||||
export async function loader({ params, context, request }: Route.LoaderArgs) {
|
export async function loader({ params, context, request }: Route.LoaderArgs) {
|
||||||
const cloudflare = context.get(cloudflareContext)
|
const cloudflare = context.get(cloudflareContext)
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ def _generate_cert(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
logger.info('Sending data to Paperforge API')
|
||||||
# Send template URI and data to Paperforge API to generate a PDF
|
# Send template URI and data to Paperforge API to generate a PDF
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
PAPERFORGE_API,
|
PAPERFORGE_API,
|
||||||
@@ -165,7 +166,7 @@ def _generate_cert(
|
|||||||
ContentType='application/pdf',
|
ContentType='application/pdf',
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f'PDF uploaded successfully to {s3_uri}')
|
logger.info(f'PDF uploaded successfully to {s3_uri}')
|
||||||
except requests.exceptions.RequestException as exc:
|
except requests.exceptions.RequestException as exc:
|
||||||
logger.exception(exc)
|
logger.exception(exc)
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -394,7 +394,7 @@ Resources:
|
|||||||
Properties:
|
Properties:
|
||||||
Handler: events.issue_cert.lambda_handler
|
Handler: events.issue_cert.lambda_handler
|
||||||
Tracing: Active
|
Tracing: Active
|
||||||
# Timeout: 12
|
Timeout: 10
|
||||||
LoggingConfig:
|
LoggingConfig:
|
||||||
LogGroup: !Ref EventLog
|
LogGroup: !Ref EventLog
|
||||||
Policies:
|
Policies:
|
||||||
|
|||||||
Reference in New Issue
Block a user