add enrollments to order
This commit is contained in:
@@ -114,7 +114,7 @@ def enroll(
|
|||||||
with processor(immediate, enroll_now, ctx) as batch:
|
with processor(immediate, enroll_now, ctx) as batch:
|
||||||
now_out = batch.process()
|
now_out = batch.process()
|
||||||
|
|
||||||
with processor(later, enroll_later, ctx) as batch:
|
with processor(later, _enroll_later, ctx) as batch:
|
||||||
later_out = batch.process()
|
later_out = batch.process()
|
||||||
|
|
||||||
def fmt(r):
|
def fmt(r):
|
||||||
@@ -317,7 +317,7 @@ def enroll_now(enrollment: Enrollment, context: Context):
|
|||||||
return enrollment
|
return enrollment
|
||||||
|
|
||||||
|
|
||||||
def enroll_later(enrollment: Enrollment, context: Context):
|
def _enroll_later(enrollment: Enrollment, context: Context):
|
||||||
now_ = now()
|
now_ = now()
|
||||||
user = enrollment.user
|
user = enrollment.user
|
||||||
course = enrollment.course
|
course = enrollment.course
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from pydantic import UUID4
|
|||||||
|
|
||||||
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 ENROLLMENT_TABLE, ORDER_TABLE
|
||||||
from exceptions import ConflictError, OrderConflictError, OrderNotFoundError
|
from exceptions import ConflictError, OrderConflictError, OrderNotFoundError
|
||||||
from middlewares.authentication_middleware import User as Authenticated
|
from middlewares.authentication_middleware import User as Authenticated
|
||||||
|
|
||||||
@@ -48,14 +48,24 @@ def get_order(order_id: str):
|
|||||||
if not order:
|
if not order:
|
||||||
raise OrderNotFoundError('Order not found')
|
raise OrderNotFoundError('Order not found')
|
||||||
|
|
||||||
|
org_id = order.get('org_id')
|
||||||
attempts = dyn.collection.query(KeyPair(order_id, 'TRANSACTION#ATTEMPT#'))
|
attempts = dyn.collection.query(KeyPair(order_id, 'TRANSACTION#ATTEMPT#'))
|
||||||
enrollments = dyn.collection.query(KeyPair(order_id, 'ENROLLMENT#'))
|
enrollments = dyn.collection.query(KeyPair(order_id, 'ENROLLMENT#'))
|
||||||
|
seats = (
|
||||||
|
dyn.collection.query(
|
||||||
|
key=KeyPair(f'SEAT#ORG#{org_id}', f'ORDER#{order_id}'),
|
||||||
|
table_name=ENROLLMENT_TABLE,
|
||||||
|
)
|
||||||
|
if org_id
|
||||||
|
else {'items': []}
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
order
|
order
|
||||||
| {
|
| {
|
||||||
'payment_attempts': attempts['items'],
|
'payment_attempts': attempts['items'],
|
||||||
'enrollments': enrollments['items'],
|
'enrollments': enrollments['items'],
|
||||||
|
'seats': seats['items'],
|
||||||
}
|
}
|
||||||
# Post-migration (orders): remove the following lines
|
# Post-migration (orders): remove the following lines
|
||||||
| ({'created_at': order['create_date']} if 'create_date' in order else {})
|
| ({'created_at': order['create_date']} if 'create_date' in order else {})
|
||||||
|
|||||||
@@ -356,5 +356,7 @@ def _get_settings(id: str) -> Settings:
|
|||||||
|
|
||||||
if 'due_days' not in r:
|
if 'due_days' not in r:
|
||||||
r['due_days'] = DUE_DAYS
|
r['due_days'] = DUE_DAYS
|
||||||
|
else:
|
||||||
|
r['due_days'] = int(r['due_days'])
|
||||||
|
|
||||||
return cast(Settings, r)
|
return cast(Settings, r)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from pydantic import FutureDatetime
|
|||||||
from api_gateway import JSONResponse
|
from api_gateway import JSONResponse
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import ENROLLMENT_TABLE
|
from config import ENROLLMENT_TABLE
|
||||||
from middlewares.authentication_middleware import User as Authenticated
|
from routes.orgs import billing
|
||||||
|
|
||||||
from ...enrollments.enroll import Context, Enrollment, Org, Subscription, enroll_now
|
from ...enrollments.enroll import Context, Enrollment, Org, Subscription, enroll_now
|
||||||
|
|
||||||
@@ -74,24 +74,20 @@ def proceed(
|
|||||||
exc_cls=ScheduledNotFoundError,
|
exc_cls=ScheduledNotFoundError,
|
||||||
)
|
)
|
||||||
billing_day = scheduled.get('subscription_billing_day')
|
billing_day = scheduled.get('subscription_billing_day')
|
||||||
ctx = cast(
|
ctx: Context = {
|
||||||
Context,
|
'created_by': router.context['user'],
|
||||||
{
|
'org': Org(id=org_id, name=scheduled['org_name']),
|
||||||
'created_by': router.context['user'],
|
}
|
||||||
'org': Org(id=org_id, name=scheduled['org_name']),
|
|
||||||
**(
|
if billing_day:
|
||||||
{'subscription': Subscription(billing_day=billing_day)}
|
ctx['subscription'] = Subscription(billing_day=billing_day)
|
||||||
if billing_day
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
enrollment = enroll_now(
|
enrollment = enroll_now(
|
||||||
Enrollment(
|
Enrollment(
|
||||||
user=scheduled['user'],
|
user=scheduled['user'],
|
||||||
course=scheduled['course'],
|
course=scheduled['course'],
|
||||||
|
seat=scheduled.get('seat'),
|
||||||
),
|
),
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import {
|
||||||
|
BanIcon,
|
||||||
|
CheckCircle2Icon,
|
||||||
|
CircleDashedIcon,
|
||||||
|
ClockIcon,
|
||||||
|
HelpCircleIcon,
|
||||||
|
type LucideIcon
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
import { Abbr } from '@repo/ui/components/abbr'
|
||||||
|
import { DateTime } from '@repo/ui/components/datetime'
|
||||||
|
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
|
||||||
|
import { Badge } from '@repo/ui/components/ui/badge'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle
|
||||||
|
} from '@repo/ui/components/ui/card'
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow
|
||||||
|
} from '@repo/ui/components/ui/table'
|
||||||
|
import { cn, initials } from '@repo/ui/lib/utils'
|
||||||
|
import type { Enrollment, Seat } from './route'
|
||||||
|
|
||||||
|
const dtOptions: Intl.DateTimeFormatOptions = {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Enrollments({
|
||||||
|
enrollments,
|
||||||
|
seats
|
||||||
|
}: {
|
||||||
|
enrollments: Enrollment[]
|
||||||
|
seats: Seat[]
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card className="lg:max-w-4xl mx-auto">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl">Matrículas relacionadas</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Acompanhe o status e os detalhes de todas as matrículas relacionadas a
|
||||||
|
esta compra.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<Table className="pointer-events-none">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Colaborador</TableHead>
|
||||||
|
<TableHead>Curso</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Executada em</TableHead>
|
||||||
|
<TableHead>Agendada em</TableHead>
|
||||||
|
<TableHead>Revogada em</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{enrollments.map(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
user,
|
||||||
|
course,
|
||||||
|
status,
|
||||||
|
executed_at,
|
||||||
|
scheduled_at,
|
||||||
|
rollback_at
|
||||||
|
},
|
||||||
|
idx
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<TableRow key={idx}>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex gap-2.5 items-center">
|
||||||
|
<Avatar className="size-10 hidden lg:block">
|
||||||
|
<AvatarFallback className="border">
|
||||||
|
{initials(user.name)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li className="font-bold">
|
||||||
|
<Abbr>{user.name}</Abbr>
|
||||||
|
</li>
|
||||||
|
<li className="text-muted-foreground text-sm">
|
||||||
|
<Abbr>{user.email}</Abbr>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Abbr>{course.name}</Abbr>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Status status={status} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{executed_at ? (
|
||||||
|
<DateTime options={dtOptions}>{executed_at}</DateTime>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{scheduled_at ? (
|
||||||
|
<DateTime options={dtOptions}>{scheduled_at}</DateTime>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{rollback_at ? (
|
||||||
|
<DateTime options={dtOptions}>{rollback_at}</DateTime>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const statuses: Record<string, { icon: LucideIcon; color?: string }> = {
|
||||||
|
PENDING: {
|
||||||
|
icon: CircleDashedIcon,
|
||||||
|
color: 'text-blue-400 [&_svg]:text-blue-500'
|
||||||
|
},
|
||||||
|
SCHEDULED: {
|
||||||
|
icon: ClockIcon,
|
||||||
|
color: 'text-blue-400 [&_svg]:text-blue-500'
|
||||||
|
},
|
||||||
|
EXECUTED: {
|
||||||
|
icon: CheckCircle2Icon,
|
||||||
|
color: 'text-green-400 [&_svg]:text-green-500'
|
||||||
|
},
|
||||||
|
ROLLBACK: {
|
||||||
|
icon: BanIcon,
|
||||||
|
color: 'text-orange-400 [&_svg]:text-orange-500'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
PENDING: 'Pendente',
|
||||||
|
EXECUTED: 'Executado',
|
||||||
|
SCHEDULED: 'Agendado',
|
||||||
|
ROLLBACK: 'Revogado'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Status({ status: s }: { status: string }) {
|
||||||
|
const status = labels[s] ?? s
|
||||||
|
const { icon: Icon, color } = statuses?.[s] ?? { icon: HelpCircleIcon }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge variant="outline" className={cn(color, 'px-1.5')}>
|
||||||
|
<Icon className={cn('stroke-2', color)} />
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -17,7 +17,10 @@ import { useForm } from 'react-hook-form'
|
|||||||
import { Link, useRevalidator } from 'react-router'
|
import { Link, useRevalidator } from 'react-router'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import { Abbr } from '@repo/ui/components/abbr'
|
||||||
import { Currency } from '@repo/ui/components/currency'
|
import { Currency } from '@repo/ui/components/currency'
|
||||||
|
import { DateTime } from '@repo/ui/components/datetime'
|
||||||
|
import { Badge } from '@repo/ui/components/ui/badge'
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
@@ -26,34 +29,13 @@ import {
|
|||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator
|
BreadcrumbSeparator
|
||||||
} from '@repo/ui/components/ui/breadcrumb'
|
} from '@repo/ui/components/ui/breadcrumb'
|
||||||
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle
|
CardTitle
|
||||||
} from '@repo/ui/components/ui/card'
|
} from '@repo/ui/components/ui/card'
|
||||||
import {
|
|
||||||
Item,
|
|
||||||
ItemActions,
|
|
||||||
ItemContent,
|
|
||||||
ItemGroup,
|
|
||||||
ItemTitle
|
|
||||||
} from '@repo/ui/components/ui/item'
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableFooter,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow
|
|
||||||
} from '@repo/ui/components/ui/table'
|
|
||||||
import { request as req } from '@repo/util/request'
|
|
||||||
|
|
||||||
import { Abbr } from '@repo/ui/components/abbr'
|
|
||||||
import { DateTime } from '@repo/ui/components/datetime'
|
|
||||||
import { Badge } from '@repo/ui/components/ui/badge'
|
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
@@ -64,6 +46,13 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger
|
DialogTrigger
|
||||||
} from '@repo/ui/components/ui/dialog'
|
} from '@repo/ui/components/ui/dialog'
|
||||||
|
import {
|
||||||
|
Item,
|
||||||
|
ItemActions,
|
||||||
|
ItemContent,
|
||||||
|
ItemGroup,
|
||||||
|
ItemTitle
|
||||||
|
} from '@repo/ui/components/ui/item'
|
||||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -72,12 +61,22 @@ import {
|
|||||||
} from '@repo/ui/components/ui/popover'
|
} 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 {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow
|
||||||
|
} from '@repo/ui/components/ui/table'
|
||||||
import { cn } from '@repo/ui/lib/utils'
|
import { cn } from '@repo/ui/lib/utils'
|
||||||
import {
|
import {
|
||||||
labels,
|
labels,
|
||||||
statuses,
|
statuses,
|
||||||
type Order as Order_
|
type Order as Order_
|
||||||
} from '@repo/ui/routes/orders/data'
|
} from '@repo/ui/routes/orders/data'
|
||||||
|
import { request as req } from '@repo/util/request'
|
||||||
import {
|
import {
|
||||||
CreditCard,
|
CreditCard,
|
||||||
creditCardSchema,
|
creditCardSchema,
|
||||||
@@ -85,6 +84,7 @@ import {
|
|||||||
} from '../_.$orgid.enrollments.buy/payment'
|
} 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'
|
||||||
|
import { Enrollments } from './enrollments'
|
||||||
|
|
||||||
export function meta() {
|
export function meta() {
|
||||||
return [
|
return [
|
||||||
@@ -131,6 +131,24 @@ type Attempts = {
|
|||||||
last4: string
|
last4: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Course = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Enrollment = {
|
||||||
|
status: 'PENDING' | 'EXECUTED' | 'ROLLBACK'
|
||||||
|
user: { id: string; name: string; email: string }
|
||||||
|
course: Course
|
||||||
|
executed_at?: string
|
||||||
|
rollback_at?: string
|
||||||
|
scheduled_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Seat = {
|
||||||
|
course: Course
|
||||||
|
}
|
||||||
|
|
||||||
type Order = Order_ & {
|
type Order = Order_ & {
|
||||||
items: Item[]
|
items: Item[]
|
||||||
interest_amount: number
|
interest_amount: number
|
||||||
@@ -142,6 +160,8 @@ type Order = Order_ & {
|
|||||||
payment_attempts: Attempts[]
|
payment_attempts: Attempts[]
|
||||||
credit_card?: CreditCardProps
|
credit_card?: CreditCardProps
|
||||||
coupon?: string
|
coupon?: string
|
||||||
|
enrollments?: Enrollment[]
|
||||||
|
seats?: Seat[]
|
||||||
installments?: number
|
installments?: number
|
||||||
created_by?: User
|
created_by?: User
|
||||||
invoice: Invoice
|
invoice: Invoice
|
||||||
@@ -173,6 +193,8 @@ export default function Route({ loaderData: { order } }: Route.ComponentProps) {
|
|||||||
discount,
|
discount,
|
||||||
invoice,
|
invoice,
|
||||||
payment_attempts = [],
|
payment_attempts = [],
|
||||||
|
enrollments = [],
|
||||||
|
seats = [],
|
||||||
items = [],
|
items = [],
|
||||||
subtotal
|
subtotal
|
||||||
} = order
|
} = order
|
||||||
@@ -185,7 +207,7 @@ export default function Route({ loaderData: { order } }: Route.ComponentProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset()
|
reset()
|
||||||
}, [])
|
}, [])
|
||||||
|
console.log(seats)
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
@@ -320,6 +342,10 @@ export default function Route({ loaderData: { order } }: Route.ComponentProps) {
|
|||||||
</Table>
|
</Table>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{enrollments.length > 0 ? (
|
||||||
|
<Enrollments enrollments={enrollments} seats={seats} />
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from pydantic import (
|
|||||||
)
|
)
|
||||||
from typing_extensions import NotRequired
|
from typing_extensions import NotRequired
|
||||||
|
|
||||||
from config import DEDUP_WINDOW_OFFSET_DAYS, USER_TABLE
|
from config import DEDUP_WINDOW_OFFSET_DAYS, ORDER_TABLE, USER_TABLE
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
@@ -107,6 +107,11 @@ class SeatNotFoundError(Exception):
|
|||||||
super().__init__('Seat required')
|
super().__init__('Seat required')
|
||||||
|
|
||||||
|
|
||||||
|
class OrderNotFoundError(Exception):
|
||||||
|
def __init__(self, msg: str | dict):
|
||||||
|
super().__init__('Order not found')
|
||||||
|
|
||||||
|
|
||||||
def enroll(
|
def enroll(
|
||||||
enrollment: Enrollment,
|
enrollment: Enrollment,
|
||||||
*,
|
*,
|
||||||
@@ -150,6 +155,26 @@ def enroll(
|
|||||||
| ({'seat': seat} if seat else {})
|
| ({'seat': seat} if seat else {})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if seat:
|
||||||
|
transact.condition(
|
||||||
|
key=KeyPair(str(seat['order_id']), '0'),
|
||||||
|
cond_expr='attribute_exists(sk)',
|
||||||
|
exc_cls=OrderNotFoundError,
|
||||||
|
table_name=ORDER_TABLE,
|
||||||
|
)
|
||||||
|
transact.put(
|
||||||
|
item={
|
||||||
|
'id': seat['order_id'],
|
||||||
|
'sk': f'ENROLLMENT#{enrollment.id}',
|
||||||
|
'course': course.model_dump(),
|
||||||
|
'user': user.model_dump(),
|
||||||
|
'status': 'EXECUTED',
|
||||||
|
'executed_at': now_,
|
||||||
|
'created_at': now_,
|
||||||
|
},
|
||||||
|
table_name=ORDER_TABLE,
|
||||||
|
)
|
||||||
|
|
||||||
# Relationships between this enrollment and its related entities
|
# Relationships between this enrollment and its related entities
|
||||||
for entity in linked_entities:
|
for entity in linked_entities:
|
||||||
# Parent knows the child
|
# Parent knows the child
|
||||||
|
|||||||
2
enrollments-events/uv.lock
generated
2
enrollments-events/uv.lock
generated
@@ -683,7 +683,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.13.1"
|
version = "0.13.4"
|
||||||
source = { directory = "../layercake" }
|
source = { directory = "../layercake" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
org_id=org_id,
|
org_id=org_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.debug('Some enrollments failed', failed=failed)
|
||||||
|
|
||||||
with dyn.transact_writer() as transact:
|
with dyn.transact_writer() as transact:
|
||||||
for x in failed:
|
for x in failed:
|
||||||
reason = _friendly_reason(x.cause['type']) # type: ignore
|
reason = _friendly_reason(x.cause['type']) # type: ignore
|
||||||
@@ -163,7 +165,7 @@ def _release_seats(
|
|||||||
) -> None:
|
) -> None:
|
||||||
now_ = now()
|
now_ = now()
|
||||||
|
|
||||||
with dyn.transact_writer(table_name=ORDER_TABLE) as transact:
|
with dyn.transact_writer(table_name=ENROLLMENT_TABLE) as transact:
|
||||||
for course in courses:
|
for course in courses:
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
@@ -172,7 +174,6 @@ def _release_seats(
|
|||||||
'course': course.model_dump(),
|
'course': course.model_dump(),
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
},
|
},
|
||||||
table_name=ORDER_TABLE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -263,6 +264,26 @@ def _enroll_now(enrollment: Enrollment, context: Context) -> None:
|
|||||||
'seat': {'order_id': order_id},
|
'seat': {'order_id': order_id},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
# Relationships between this enrollment and its related entities
|
||||||
|
transact.put(
|
||||||
|
item={
|
||||||
|
'id': order_id,
|
||||||
|
'sk': f'LINKED_ENTITIES#CHILD#ENROLLMENT#{enrollment.id}',
|
||||||
|
'created_at': now_,
|
||||||
|
},
|
||||||
|
cond_expr='attribute_not_exists(sk)',
|
||||||
|
table_name=ORDER_TABLE,
|
||||||
|
)
|
||||||
|
# Child knows the parent
|
||||||
|
transact.put(
|
||||||
|
item={
|
||||||
|
'id': enrollment.id,
|
||||||
|
'sk': f'LINKED_ENTITIES#PARENT#ORDER#{order_id}',
|
||||||
|
'created_at': now_,
|
||||||
|
},
|
||||||
|
cond_expr='attribute_not_exists(sk)',
|
||||||
|
)
|
||||||
|
|
||||||
transact.update(
|
transact.update(
|
||||||
key=KeyPair(order_id, f'ENROLLMENT#{enrollment.id}'),
|
key=KeyPair(order_id, f'ENROLLMENT#{enrollment.id}'),
|
||||||
update_expr='SET #status = :executed, \
|
update_expr='SET #status = :executed, \
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ Resources:
|
|||||||
LoggingConfig:
|
LoggingConfig:
|
||||||
LogGroup: !Ref HttpLog
|
LogGroup: !Ref HttpLog
|
||||||
Policies:
|
Policies:
|
||||||
- DynamoDBWritePolicy:
|
- DynamoDBCrudPolicy:
|
||||||
TableName: !Ref OrderTable
|
TableName: !Ref OrderTable
|
||||||
Events:
|
Events:
|
||||||
Post:
|
Post:
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
CheckCircle2Icon,
|
|
||||||
ClockAlertIcon,
|
|
||||||
RotateCcwIcon,
|
|
||||||
CircleXIcon,
|
|
||||||
ClockIcon,
|
|
||||||
BanIcon,
|
BanIcon,
|
||||||
|
CheckCircle2Icon,
|
||||||
|
CircleXIcon,
|
||||||
|
ClockAlertIcon,
|
||||||
|
ClockIcon,
|
||||||
|
RotateCcwIcon,
|
||||||
type LucideIcon
|
type LucideIcon
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
@@ -21,39 +21,30 @@ export type Order = {
|
|||||||
email: string
|
email: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const statuses: Record<
|
export const statuses: Record<string, { icon: LucideIcon; color?: string }> = {
|
||||||
string,
|
|
||||||
{ icon: LucideIcon; color?: string; label: string }
|
|
||||||
> = {
|
|
||||||
PENDING: {
|
PENDING: {
|
||||||
icon: ClockIcon,
|
icon: ClockIcon,
|
||||||
label: 'Pendente',
|
|
||||||
color: 'text-blue-400 [&_svg]:text-blue-500'
|
color: 'text-blue-400 [&_svg]:text-blue-500'
|
||||||
},
|
},
|
||||||
PAID: {
|
PAID: {
|
||||||
icon: CheckCircle2Icon,
|
icon: CheckCircle2Icon,
|
||||||
color: 'text-green-400 [&_svg]:text-green-500',
|
color: 'text-green-400 [&_svg]:text-green-500'
|
||||||
label: 'Pago'
|
|
||||||
},
|
},
|
||||||
DECLINED: {
|
DECLINED: {
|
||||||
icon: BanIcon,
|
icon: BanIcon,
|
||||||
color: 'text-red-400 [&_svg]:text-red-500',
|
color: 'text-red-400 [&_svg]:text-red-500'
|
||||||
label: 'Negado'
|
|
||||||
},
|
},
|
||||||
EXPIRED: {
|
EXPIRED: {
|
||||||
icon: ClockAlertIcon,
|
icon: ClockAlertIcon,
|
||||||
color: 'text-orange-400 [&_svg]:text-orange-500',
|
color: 'text-orange-400 [&_svg]:text-orange-500'
|
||||||
label: 'Expirado'
|
|
||||||
},
|
},
|
||||||
REFUNDED: {
|
REFUNDED: {
|
||||||
icon: RotateCcwIcon,
|
icon: RotateCcwIcon,
|
||||||
color: 'text-orange-400 [&_svg]:text-orange-500',
|
color: 'text-orange-400 [&_svg]:text-orange-500'
|
||||||
label: 'Estornado'
|
|
||||||
},
|
},
|
||||||
CANCELED: {
|
CANCELED: {
|
||||||
icon: CircleXIcon,
|
icon: CircleXIcon,
|
||||||
color: 'text-red-400 [&_svg]:text-red-500',
|
color: 'text-red-400 [&_svg]:text-red-500'
|
||||||
label: 'Cancelado'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user