WIP
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
from datetime import date, datetime, time, timedelta
|
from datetime import date, datetime, time, timedelta
|
||||||
from typing import Annotated, TypedDict
|
from typing import Annotated, NotRequired, TypedDict
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
@@ -16,7 +16,12 @@ from pydantic import UUID4, BaseModel, EmailStr, Field, FutureDate
|
|||||||
|
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import DEDUP_WINDOW_OFFSET_DAYS, ENROLLMENT_TABLE, TZ, USER_TABLE
|
from config import DEDUP_WINDOW_OFFSET_DAYS, ENROLLMENT_TABLE, TZ, USER_TABLE
|
||||||
from exceptions import ConflictError, SubscriptionFrozenError, SubscriptionRequiredError
|
from exceptions import (
|
||||||
|
ConflictError,
|
||||||
|
SubscriptionConflictError,
|
||||||
|
SubscriptionFrozenError,
|
||||||
|
SubscriptionRequiredError,
|
||||||
|
)
|
||||||
from middlewares.authentication_middleware import User as Authenticated
|
from middlewares.authentication_middleware import User as Authenticated
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
@@ -49,12 +54,30 @@ class Subscription(BaseModel):
|
|||||||
billing_day: int
|
billing_day: int
|
||||||
|
|
||||||
|
|
||||||
|
class Seat(BaseModel):
|
||||||
|
id: str = Field(..., pattern=r'^SEAT#ORG#.+$')
|
||||||
|
sk: str = Field(..., pattern=r'^ORDER#.+#ENROLLMENT#.+$')
|
||||||
|
|
||||||
|
def org_id(self) -> str:
|
||||||
|
*_, org_id = self.id.split('#')
|
||||||
|
return org_id
|
||||||
|
|
||||||
|
def order_id(self) -> str:
|
||||||
|
_, order_id, *_ = self.sk.split('#')
|
||||||
|
return order_id
|
||||||
|
|
||||||
|
def enrollment_id(self) -> str:
|
||||||
|
*_, enrollment_id = self.sk.split('#')
|
||||||
|
return enrollment_id
|
||||||
|
|
||||||
|
|
||||||
class Enrollment(BaseModel):
|
class Enrollment(BaseModel):
|
||||||
id: UUID4 = Field(default_factory=uuid4)
|
id: UUID4 = Field(default_factory=uuid4)
|
||||||
user: User
|
user: User
|
||||||
course: Course
|
course: Course
|
||||||
scheduled_for: FutureDate | None = None
|
scheduled_for: FutureDate | None = None
|
||||||
deduplication_window: DeduplicationWindow | None = None
|
deduplication_window: DeduplicationWindow | None = None
|
||||||
|
seat: Seat | None = None
|
||||||
|
|
||||||
|
|
||||||
class Org(BaseModel):
|
class Org(BaseModel):
|
||||||
@@ -66,30 +89,22 @@ class Org(BaseModel):
|
|||||||
def enroll(
|
def enroll(
|
||||||
org_id: Annotated[str | UUID4, Body(embed=True)],
|
org_id: Annotated[str | UUID4, Body(embed=True)],
|
||||||
enrollments: Annotated[tuple[Enrollment, ...], Body(embed=True)],
|
enrollments: Annotated[tuple[Enrollment, ...], Body(embed=True)],
|
||||||
|
subscription: Annotated[Subscription | None, Body(embed=True)] = None,
|
||||||
):
|
):
|
||||||
now_ = now()
|
now_ = now()
|
||||||
created_by: Authenticated = router.context['user']
|
created_by: Authenticated = router.context['user']
|
||||||
org = dyn.collection.get_items(
|
org = dyn.collection.get_item(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
pk=str(org_id),
|
pk=str(org_id),
|
||||||
sk='0',
|
sk='0',
|
||||||
table_name=USER_TABLE,
|
table_name=USER_TABLE,
|
||||||
)
|
)
|
||||||
+ KeyPair(
|
|
||||||
pk=str(org_id),
|
|
||||||
sk='METADATA#SUBSCRIPTION',
|
|
||||||
rename_key='subscription',
|
|
||||||
table_name=USER_TABLE,
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if 'subscription' not in org:
|
|
||||||
raise SubscriptionRequiredError('Organization not subscribed')
|
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
'org': Org.model_validate(org),
|
'org': Org.model_validate(org),
|
||||||
'created_by': created_by,
|
'created_by': created_by,
|
||||||
'subscription': Subscription.model_validate(org['subscription']),
|
'subscription': subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
immediate = [e for e in enrollments if not e.scheduled_for]
|
immediate = [e for e in enrollments if not e.scheduled_for]
|
||||||
@@ -133,8 +148,8 @@ Context = TypedDict(
|
|||||||
'Context',
|
'Context',
|
||||||
{
|
{
|
||||||
'org': Org,
|
'org': Org,
|
||||||
'subscription': Subscription,
|
|
||||||
'created_by': Authenticated,
|
'created_by': Authenticated,
|
||||||
|
'subscription': NotRequired[Subscription],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -144,7 +159,7 @@ def enroll_now(enrollment: Enrollment, context: Context):
|
|||||||
user = enrollment.user
|
user = enrollment.user
|
||||||
course = enrollment.course
|
course = enrollment.course
|
||||||
org: Org = context['org']
|
org: Org = context['org']
|
||||||
subscription: Subscription = context['subscription']
|
subscription: Subscription | None = context.get('subscription')
|
||||||
created_by: Authenticated = context['created_by']
|
created_by: Authenticated = context['created_by']
|
||||||
lock_hash = md5_hash(f'{user.id}{course.id}')
|
lock_hash = md5_hash(f'{user.id}{course.id}')
|
||||||
access_expires_at = now_ + timedelta(days=course.access_period)
|
access_expires_at = now_ + timedelta(days=course.access_period)
|
||||||
@@ -160,24 +175,6 @@ def enroll_now(enrollment: Enrollment, context: Context):
|
|||||||
)
|
)
|
||||||
|
|
||||||
with dyn.transact_writer() as transact:
|
with dyn.transact_writer() as transact:
|
||||||
transact.condition(
|
|
||||||
key=KeyPair(
|
|
||||||
pk='SUBSCRIPTION',
|
|
||||||
sk=f'ORG#{org.id}',
|
|
||||||
),
|
|
||||||
cond_expr='attribute_exists(sk)',
|
|
||||||
exc_cls=SubscriptionRequiredError,
|
|
||||||
table_name=USER_TABLE,
|
|
||||||
)
|
|
||||||
transact.condition(
|
|
||||||
key=KeyPair(
|
|
||||||
pk='SUBSCRIPTION#FROZEN',
|
|
||||||
sk=f'ORG#{org.id}',
|
|
||||||
),
|
|
||||||
cond_expr='attribute_not_exists(sk)',
|
|
||||||
exc_cls=SubscriptionFrozenError,
|
|
||||||
table_name=USER_TABLE,
|
|
||||||
)
|
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': enrollment.id,
|
'id': enrollment.id,
|
||||||
@@ -209,15 +206,6 @@ def enroll_now(enrollment: Enrollment, context: Context):
|
|||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
transact.put(
|
|
||||||
item={
|
|
||||||
'id': enrollment.id,
|
|
||||||
'sk': 'METADATA#SUBSCRIPTION_COVERED',
|
|
||||||
'org_id': org.id,
|
|
||||||
'billing_day': subscription.billing_day,
|
|
||||||
'created_at': now_,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': enrollment.id,
|
'id': enrollment.id,
|
||||||
@@ -248,6 +236,38 @@ def enroll_now(enrollment: Enrollment, context: Context):
|
|||||||
exc_cls=DeduplicationConflictError,
|
exc_cls=DeduplicationConflictError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if subscription:
|
||||||
|
transact.put(
|
||||||
|
item={
|
||||||
|
'id': enrollment.id,
|
||||||
|
'sk': 'METADATA#SUBSCRIPTION_COVERED',
|
||||||
|
'org_id': org.id,
|
||||||
|
'billing_day': subscription.billing_day,
|
||||||
|
'created_at': now_,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
transact.condition(
|
||||||
|
key=KeyPair('SUBSCRIPTION', f'ORG#{org.id}'),
|
||||||
|
cond_expr='attribute_exists(sk)',
|
||||||
|
exc_cls=SubscriptionRequiredError,
|
||||||
|
table_name=USER_TABLE,
|
||||||
|
)
|
||||||
|
transact.condition(
|
||||||
|
key=KeyPair(str(org.id), 'METADATA#SUBSCRIPTION'),
|
||||||
|
cond_expr='billing_day = :billing_day',
|
||||||
|
expr_attr_values={
|
||||||
|
':billing_day': subscription.billing_day,
|
||||||
|
},
|
||||||
|
exc_cls=SubscriptionConflictError,
|
||||||
|
table_name=USER_TABLE,
|
||||||
|
)
|
||||||
|
transact.condition(
|
||||||
|
key=KeyPair('SUBSCRIPTION#FROZEN', f'ORG#{org.id}'),
|
||||||
|
cond_expr='attribute_not_exists(sk)',
|
||||||
|
exc_cls=SubscriptionFrozenError,
|
||||||
|
table_name=USER_TABLE,
|
||||||
|
)
|
||||||
|
|
||||||
# The deduplication window can be recalculated based on user settings.
|
# The deduplication window can be recalculated based on user settings.
|
||||||
if deduplication_window:
|
if deduplication_window:
|
||||||
transact.put(
|
transact.put(
|
||||||
@@ -267,9 +287,9 @@ def enroll_later(enrollment: Enrollment, context: Context):
|
|||||||
user = enrollment.user
|
user = enrollment.user
|
||||||
course = enrollment.course
|
course = enrollment.course
|
||||||
scheduled_for = date_to_midnight(enrollment.scheduled_for) # type: ignore
|
scheduled_for = date_to_midnight(enrollment.scheduled_for) # type: ignore
|
||||||
deduplication_window = enrollment.deduplication_window
|
dedup_window = enrollment.deduplication_window
|
||||||
org: Org = context['org']
|
org: Org = context['org']
|
||||||
subscription: Subscription = context['subscription']
|
subscription: Subscription | None = context.get('subscription')
|
||||||
created_by: Authenticated = context['created_by']
|
created_by: Authenticated = context['created_by']
|
||||||
lock_hash = md5_hash(f'{user.id}{course.id}')
|
lock_hash = md5_hash(f'{user.id}{course.id}')
|
||||||
|
|
||||||
@@ -277,24 +297,6 @@ def enroll_later(enrollment: Enrollment, context: Context):
|
|||||||
pk = f'SCHEDULED#ORG#{org.id}'
|
pk = f'SCHEDULED#ORG#{org.id}'
|
||||||
sk = f'{scheduled_for.isoformat()}#{lock_hash}'
|
sk = f'{scheduled_for.isoformat()}#{lock_hash}'
|
||||||
|
|
||||||
transact.condition(
|
|
||||||
key=KeyPair(
|
|
||||||
pk='SUBSCRIPTION',
|
|
||||||
sk=f'ORG#{org.id}',
|
|
||||||
),
|
|
||||||
cond_expr='attribute_exists(sk)',
|
|
||||||
exc_cls=SubscriptionRequiredError,
|
|
||||||
table_name=USER_TABLE,
|
|
||||||
)
|
|
||||||
transact.condition(
|
|
||||||
key=KeyPair(
|
|
||||||
pk='SUBSCRIPTION#FROZEN',
|
|
||||||
sk=f'ORG#{org.id}',
|
|
||||||
),
|
|
||||||
cond_expr='attribute_not_exists(sk)',
|
|
||||||
exc_cls=SubscriptionFrozenError,
|
|
||||||
table_name=USER_TABLE,
|
|
||||||
)
|
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': pk,
|
'id': pk,
|
||||||
@@ -306,13 +308,19 @@ def enroll_later(enrollment: Enrollment, context: Context):
|
|||||||
'id': created_by.id,
|
'id': created_by.id,
|
||||||
'name': created_by.name,
|
'name': created_by.name,
|
||||||
},
|
},
|
||||||
'subscription_billing_day': subscription.billing_day,
|
|
||||||
'ttl': ttl(start_dt=scheduled_for),
|
'ttl': ttl(start_dt=scheduled_for),
|
||||||
'scheduled_at': now_,
|
'scheduled_at': now_,
|
||||||
}
|
}
|
||||||
| (
|
| (
|
||||||
{'dedup_window_offset_days': deduplication_window.offset_days}
|
{'dedup_window_offset_days': dedup_window.offset_days}
|
||||||
if deduplication_window
|
if dedup_window
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
| (
|
||||||
|
{
|
||||||
|
'subscription_billing_day': subscription.billing_day,
|
||||||
|
}
|
||||||
|
if subscription
|
||||||
else {}
|
else {}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -331,6 +339,28 @@ def enroll_later(enrollment: Enrollment, context: Context):
|
|||||||
exc_cls=DeduplicationConflictError,
|
exc_cls=DeduplicationConflictError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if subscription:
|
||||||
|
transact.condition(
|
||||||
|
key=KeyPair('SUBSCRIPTION', f'ORG#{org.id}'),
|
||||||
|
cond_expr='attribute_exists(sk)',
|
||||||
|
exc_cls=SubscriptionRequiredError,
|
||||||
|
table_name=USER_TABLE,
|
||||||
|
)
|
||||||
|
transact.condition(
|
||||||
|
key=KeyPair(str(org.id), 'METADATA#SUBSCRIPTION'),
|
||||||
|
cond_expr='billing_day = :billing_day',
|
||||||
|
expr_attr_values={
|
||||||
|
':billing_day': subscription.billing_day,
|
||||||
|
},
|
||||||
|
exc_cls=SubscriptionConflictError,
|
||||||
|
)
|
||||||
|
transact.condition(
|
||||||
|
key=KeyPair('SUBSCRIPTION#FROZEN', f'ORG#{org.id}'),
|
||||||
|
cond_expr='attribute_not_exists(sk)',
|
||||||
|
exc_cls=SubscriptionFrozenError,
|
||||||
|
table_name=USER_TABLE,
|
||||||
|
)
|
||||||
|
|
||||||
return enrollment
|
return enrollment
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -69,14 +69,14 @@ class Address(BaseModel):
|
|||||||
class Item(BaseModel):
|
class Item(BaseModel):
|
||||||
id: UUID4
|
id: UUID4
|
||||||
name: str
|
name: str
|
||||||
unit_price: Decimal
|
unit_price: Decimal = Field(..., ge=1)
|
||||||
quantity: int = 1
|
quantity: int = Field(1, ge=1)
|
||||||
|
|
||||||
|
|
||||||
class Coupon(BaseModel):
|
class Coupon(BaseModel):
|
||||||
code: str
|
code: str
|
||||||
type: Literal['PERCENT', 'FIXED']
|
type: Literal['PERCENT', 'FIXED']
|
||||||
amount: Decimal
|
amount: Decimal = Field(..., ge=1)
|
||||||
|
|
||||||
|
|
||||||
class Checkout(BaseModel):
|
class Checkout(BaseModel):
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ dev = [
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
pythonpath = ["app/"]
|
pythonpath = ["app/"]
|
||||||
addopts = "--cov --cov-report html -v"
|
addopts = "--cov=app --cov-report html"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py311"
|
target-version = "py311"
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ def test_enroll(
|
|||||||
method=HTTPMethod.POST,
|
method=HTTPMethod.POST,
|
||||||
body={
|
body={
|
||||||
'org_id': '2a8963fc-4694-4fe2-953a-316d1b10f1f5',
|
'org_id': '2a8963fc-4694-4fe2-953a-316d1b10f1f5',
|
||||||
|
'subscription': {
|
||||||
|
'billing_day': 6,
|
||||||
|
},
|
||||||
'enrollments': [
|
'enrollments': [
|
||||||
{
|
{
|
||||||
'id': '44ff9ac1-a7cd-447b-a284-53cdc5929d7f',
|
'id': '44ff9ac1-a7cd-447b-a284-53cdc5929d7f',
|
||||||
@@ -64,8 +67,6 @@ def test_enroll(
|
|||||||
submission = dynamodb_persistence_layer.get_item(KeyPair(body['id'], body['sk']))
|
submission = dynamodb_persistence_layer.get_item(KeyPair(body['id'], body['sk']))
|
||||||
assert submission['sk'] == body['sk']
|
assert submission['sk'] == body['sk']
|
||||||
|
|
||||||
print(body)
|
|
||||||
|
|
||||||
enrolled = dynamodb_persistence_layer.collection.query(
|
enrolled = dynamodb_persistence_layer.collection.query(
|
||||||
PartitionKey('d0349bbe-cef3-44f7-b20e-3cb4476ab4c5')
|
PartitionKey('d0349bbe-cef3-44f7-b20e-3cb4476ab4c5')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -243,7 +243,9 @@ function List({ items, search }) {
|
|||||||
<Abbr>{created_by ? created_by.name : 'N/A'}</Abbr>
|
<Abbr>{created_by ? created_by.name : 'N/A'}</Abbr>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<DateTime>{enrolled_at}</DateTime>
|
<DateTime options={{ hour: '2-digit', minute: '2-digit' }}>
|
||||||
|
{enrolled_at}
|
||||||
|
</DateTime>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Currency>{unit_price}</Currency>
|
<Currency>{unit_price}</Currency>
|
||||||
@@ -281,7 +283,9 @@ function List({ items, search }) {
|
|||||||
<Abbr>{canceled_by ? canceled_by.name : 'N/A'}</Abbr>
|
<Abbr>{canceled_by ? canceled_by.name : 'N/A'}</Abbr>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<DateTime>{created_at}</DateTime>
|
<DateTime options={{ hour: '2-digit', minute: '2-digit' }}>
|
||||||
|
{created_at}
|
||||||
|
</DateTime>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Currency>{unit_price}</Currency>
|
<Currency>{unit_price}</Currency>
|
||||||
|
|||||||
@@ -1,27 +1,21 @@
|
|||||||
import {
|
|
||||||
use,
|
|
||||||
useState,
|
|
||||||
useMemo,
|
|
||||||
forwardRef,
|
|
||||||
type InputHTMLAttributes
|
|
||||||
} from 'react'
|
|
||||||
import { useToggle } from 'ahooks'
|
import { useToggle } from 'ahooks'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import {
|
import {
|
||||||
ChevronsUpDownIcon,
|
|
||||||
CheckIcon,
|
|
||||||
BookIcon,
|
|
||||||
ArrowDownAZIcon,
|
ArrowDownAZIcon,
|
||||||
ArrowUpAZIcon
|
ArrowUpAZIcon,
|
||||||
|
BookIcon,
|
||||||
|
CheckIcon,
|
||||||
|
ChevronsUpDownIcon
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
import { cn } from '@repo/ui/lib/utils'
|
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
|
||||||
import {
|
import {
|
||||||
InputGroup,
|
forwardRef,
|
||||||
InputGroupAddon,
|
use,
|
||||||
InputGroupInput
|
useMemo,
|
||||||
} from '@repo/ui/components/ui/input-group'
|
useState,
|
||||||
|
type InputHTMLAttributes
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -30,11 +24,17 @@ import {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList
|
CommandList
|
||||||
} from '@repo/ui/components/ui/command'
|
} from '@repo/ui/components/ui/command'
|
||||||
|
import {
|
||||||
|
InputGroup,
|
||||||
|
InputGroupAddon,
|
||||||
|
InputGroupInput
|
||||||
|
} from '@repo/ui/components/ui/input-group'
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger
|
PopoverTrigger
|
||||||
} from '@repo/ui/components/ui/popover'
|
} from '@repo/ui/components/ui/popover'
|
||||||
|
import { cn } from '@repo/ui/lib/utils'
|
||||||
|
|
||||||
import { type Course } from './data'
|
import { type Course } from './data'
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ export const CoursePicker = forwardRef<HTMLInputElement, CoursePickerProps>(
|
|||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Curso"
|
placeholder="Digite para pesquisar"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onValueChange={setSearch}
|
onValueChange={setSearch}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from 'zod'
|
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
export const MAX_ITEMS = 50
|
export const MAX_ITEMS = 50
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
Trash2Icon
|
Trash2Icon
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { pick } from 'ramda'
|
|
||||||
import { Fragment, use, useEffect, type ReactNode } from 'react'
|
import { Fragment, use, useEffect, type ReactNode } from 'react'
|
||||||
import { Controller, useFieldArray, useForm } from 'react-hook-form'
|
import { Controller, useFieldArray, useForm } from 'react-hook-form'
|
||||||
import { Link, redirect, useFetcher, useParams } from 'react-router'
|
import { Link, redirect, useFetcher, useParams } from 'react-router'
|
||||||
@@ -115,13 +114,14 @@ export async function loader({ params, context, request }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
export async function action({ params, request, context }: Route.ActionArgs) {
|
export async function action({ params, request, context }: Route.ActionArgs) {
|
||||||
const { orgid: org_id } = params
|
const { orgid: org_id } = params
|
||||||
|
const { subscription } = context.get(workspaceContext)
|
||||||
const body = (await request.json()) as object
|
const body = (await request.json()) as object
|
||||||
|
|
||||||
const r = await req({
|
const r = await req({
|
||||||
url: `enrollments`,
|
url: `enrollments`,
|
||||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||||
method: HttpMethod.POST,
|
method: HttpMethod.POST,
|
||||||
body: JSON.stringify({ org_id, ...body }),
|
body: JSON.stringify({ org_id, subscription, ...body }),
|
||||||
request,
|
request,
|
||||||
context
|
context
|
||||||
})
|
})
|
||||||
@@ -181,7 +181,10 @@ export default function Route({
|
|||||||
'enrollments',
|
'enrollments',
|
||||||
enrolled
|
enrolled
|
||||||
.filter(({ status }) => status === 'fail')
|
.filter(({ status }) => status === 'fail')
|
||||||
.map(({ input_record }) => pick(['course', 'user'], input_record))
|
.map(({ input_record: { course, user } }) => ({
|
||||||
|
user,
|
||||||
|
course: { ...course, unit_price: 0 }
|
||||||
|
}))
|
||||||
)
|
)
|
||||||
}, [enrolled, setValue])
|
}, [enrolled, setValue])
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import type { ControllerFieldState } from 'react-hook-form'
|
|
||||||
import { XIcon, CheckIcon, AlertTriangleIcon, UserIcon } from 'lucide-react'
|
|
||||||
import { formatCPF } from '@brazilian-utils/brazilian-utils'
|
import { formatCPF } from '@brazilian-utils/brazilian-utils'
|
||||||
|
import { AlertTriangleIcon, CheckIcon, UserIcon, XIcon } from 'lucide-react'
|
||||||
|
import type { ControllerFieldState } from 'react-hook-form'
|
||||||
|
|
||||||
import { cn, initials } from '@repo/ui/lib/utils'
|
|
||||||
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
|
|
||||||
import { Abbr } from '@repo/ui/components/abbr'
|
import { Abbr } from '@repo/ui/components/abbr'
|
||||||
import { Spinner } from '@repo/ui/components/ui/spinner'
|
import { SearchFilter } from '@repo/ui/components/search-filter'
|
||||||
|
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
|
||||||
|
import { CommandItem } from '@repo/ui/components/ui/command'
|
||||||
import {
|
import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupInput
|
InputGroupInput
|
||||||
} from '@repo/ui/components/ui/input-group'
|
} from '@repo/ui/components/ui/input-group'
|
||||||
import { CommandItem } from '@repo/ui/components/ui/command'
|
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||||
import { SearchFilter } from '@repo/ui/components/search-filter'
|
import { cn, initials } from '@repo/ui/lib/utils'
|
||||||
|
|
||||||
import type { User } from './data'
|
import type { User } from './data'
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ export function UserPicker({
|
|||||||
align="start"
|
align="start"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
|
placeholder="Digite para pesquisar"
|
||||||
render={({ id, name, email, cpf, onSelect, onClose }) => (
|
render={({ id, name, email, cpf, onSelect, onClose }) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={id}
|
key={id}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:104
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:106
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from aws_lambda_powertools.event_handler.api_gateway import (
|
|||||||
)
|
)
|
||||||
from aws_lambda_powertools.logging import correlation_paths
|
from aws_lambda_powertools.logging import correlation_paths
|
||||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||||
|
from layercake.dateutils import now
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||||
|
|
||||||
from boto3clients import dynamodb_client, s3_client
|
from boto3clients import dynamodb_client, s3_client
|
||||||
@@ -53,8 +54,14 @@ def postback():
|
|||||||
dyn.update_item(
|
dyn.update_item(
|
||||||
key=KeyPair(enrollment_id, '0'),
|
key=KeyPair(enrollment_id, '0'),
|
||||||
cond_expr='attribute_exists(sk)',
|
cond_expr='attribute_exists(sk)',
|
||||||
update_expr='SET cert.s3_uri = :s3_uri',
|
update_expr='SET cert.s3_uri = :s3_uri, \
|
||||||
expr_attr_values={':s3_uri': s3_uri},
|
cert.signed = :true, \
|
||||||
|
updated_at = :now',
|
||||||
|
expr_attr_values={
|
||||||
|
':s3_uri': s3_uri,
|
||||||
|
':true': True,
|
||||||
|
':now': now(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(status_code=HTTPStatus.NO_CONTENT)
|
return Response(status_code=HTTPStatus.NO_CONTENT)
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ from aws_lambda_powertools.utilities.data_classes import (
|
|||||||
event_source,
|
event_source,
|
||||||
)
|
)
|
||||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||||
|
from layercake.dateutils import now
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer
|
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||||
|
from layercake.funcs import omit
|
||||||
from layercake.strutils import first_word
|
from layercake.strutils import first_word
|
||||||
|
|
||||||
from boto3clients import dynamodb_client, s3_client
|
from boto3clients import dynamodb_client, s3_client
|
||||||
@@ -52,7 +54,15 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
logger.debug(r)
|
|
||||||
|
dyn.put_item(
|
||||||
|
item={
|
||||||
|
'id': new_image['id'],
|
||||||
|
'sk': 'METADATA#DOCUSEAL',
|
||||||
|
'snapshot': omit(('fields',), r),
|
||||||
|
'created_at': now(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,12 @@ dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
|||||||
@logger.inject_lambda_context
|
@logger.inject_lambda_context
|
||||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||||
old_image = event.detail['old_image']
|
old_image = event.detail['old_image']
|
||||||
sk = old_image['sk']
|
|
||||||
_, lock_hash = sk.split('#')
|
|
||||||
now_ = now()
|
now_ = now()
|
||||||
# Key pattern `SCHEDULED#ORG#{org_id}`
|
# Key pattern `SCHEDULED#ORG#{org_id}`
|
||||||
*_, org_id = old_image['id'].split('#')
|
*_, org_id = old_image['id'].split('#')
|
||||||
|
# Key pattern `{YYYY-MM-DD HH:MM:SS.mmmmmm}#{lock_hash}`
|
||||||
|
sk = old_image['sk']
|
||||||
|
_, lock_hash = sk.split('#')
|
||||||
offset_days = old_image.get('dedup_window_offset_days')
|
offset_days = old_image.get('dedup_window_offset_days')
|
||||||
billing_day = old_image.get('subscription_billing_day')
|
billing_day = old_image.get('subscription_billing_day')
|
||||||
created_by = old_image.get('created_by')
|
created_by = old_image.get('created_by')
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
# from uuid import uuid4
|
|
||||||
|
|
||||||
from aws_durable_execution_sdk_python import DurableContext, durable_execution
|
|
||||||
from aws_lambda_powertools import Logger
|
|
||||||
from aws_lambda_powertools.utilities.data_classes import (
|
|
||||||
EventBridgeEvent,
|
|
||||||
event_source,
|
|
||||||
)
|
|
||||||
|
|
||||||
# from layercake.dateutils import now
|
|
||||||
|
|
||||||
logger = Logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@event_source(data_class=EventBridgeEvent)
|
|
||||||
@durable_execution
|
|
||||||
def lambda_handler(event: EventBridgeEvent, context: DurableContext) -> bool:
|
|
||||||
# new_image = event.detail['new_image']
|
|
||||||
# now_ = now()
|
|
||||||
# org_id = ''
|
|
||||||
# order_id = new_image['id']
|
|
||||||
|
|
||||||
# docx = {
|
|
||||||
# 'id': f'SEAT#ORG#{org_id}',
|
|
||||||
# 'sk': f'ORDER#{order_id}#ENROLLMENT#{uuid4()}',
|
|
||||||
# 'course': {},
|
|
||||||
# 'created_at': now_,
|
|
||||||
|
|
||||||
logger.info(event)
|
|
||||||
# }
|
|
||||||
|
|
||||||
return True
|
|
||||||
@@ -19,7 +19,7 @@ dev = [
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
pythonpath = ["app/"]
|
pythonpath = ["app/"]
|
||||||
addopts = "--cov --cov-report html -v"
|
addopts = "--cov=app --cov-report html"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py311"
|
target-version = "py311"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
AWSTemplateFormatVersion: 2010-09-09
|
AWSTemplateFormatVersion: '2010-09-09'
|
||||||
Transform: AWS::Serverless-2016-10-31
|
Transform: AWS::Serverless-2016-10-31
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
@@ -21,11 +21,11 @@ Parameters:
|
|||||||
Globals:
|
Globals:
|
||||||
Function:
|
Function:
|
||||||
CodeUri: app/
|
CodeUri: app/
|
||||||
Runtime: python3.13
|
Runtime: python3.14
|
||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:layercake:104
|
- !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:layercake:106
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
@@ -179,8 +179,10 @@ Resources:
|
|||||||
detail:
|
detail:
|
||||||
new_image:
|
new_image:
|
||||||
# Post-migration: uncomment the following lines
|
# Post-migration: uncomment the following lines
|
||||||
# sk: [SLOT]
|
# sk: [FULFILLMENT]
|
||||||
# mode: [STANDALONE]
|
# status: [IN_PROGRESS]
|
||||||
|
# user_id:
|
||||||
|
# - exists: true
|
||||||
sk: [generated_items]
|
sk: [generated_items]
|
||||||
scope: [SINGLE_USER]
|
scope: [SINGLE_USER]
|
||||||
status: [PENDING]
|
status: [PENDING]
|
||||||
@@ -194,7 +196,7 @@ Resources:
|
|||||||
Policies:
|
Policies:
|
||||||
- DynamoDBCrudPolicy:
|
- DynamoDBCrudPolicy:
|
||||||
TableName: !Ref EnrollmentTable
|
TableName: !Ref EnrollmentTable
|
||||||
- DynamoDBReadPolicy:
|
- DynamoDBCrudPolicy:
|
||||||
TableName: !Ref UserTable
|
TableName: !Ref UserTable
|
||||||
Events:
|
Events:
|
||||||
DynamoDBEvent:
|
DynamoDBEvent:
|
||||||
@@ -217,6 +219,8 @@ Resources:
|
|||||||
Policies:
|
Policies:
|
||||||
- DynamoDBCrudPolicy:
|
- DynamoDBCrudPolicy:
|
||||||
TableName: !Ref EnrollmentTable
|
TableName: !Ref EnrollmentTable
|
||||||
|
- DynamoDBCrudPolicy:
|
||||||
|
TableName: !Ref UserTable
|
||||||
Events:
|
Events:
|
||||||
DynamoDBEvent:
|
DynamoDBEvent:
|
||||||
Type: EventBridgeRule
|
Type: EventBridgeRule
|
||||||
@@ -234,6 +238,7 @@ Resources:
|
|||||||
old_image:
|
old_image:
|
||||||
status: [IN_PROGRESS]
|
status: [IN_PROGRESS]
|
||||||
|
|
||||||
|
# DEPRECATED
|
||||||
EventAllocateSlotsFunction:
|
EventAllocateSlotsFunction:
|
||||||
Type: AWS::Serverless::Function
|
Type: AWS::Serverless::Function
|
||||||
Properties:
|
Properties:
|
||||||
@@ -256,38 +261,10 @@ Resources:
|
|||||||
detail-type: [INSERT]
|
detail-type: [INSERT]
|
||||||
detail:
|
detail:
|
||||||
new_image:
|
new_image:
|
||||||
# Post-migration: uncomment the following lines
|
|
||||||
# sk: [SLOT]
|
|
||||||
# mode: [BATCH]
|
|
||||||
sk: [generated_items]
|
sk: [generated_items]
|
||||||
scope: [MULTI_USER]
|
scope: [MULTI_USER]
|
||||||
status: [PENDING]
|
status: [PENDING]
|
||||||
|
|
||||||
EventAllocateSeatsFunction:
|
|
||||||
Type: AWS::Serverless::Function
|
|
||||||
Properties:
|
|
||||||
Handler: events.allocate_seats.lambda_handler
|
|
||||||
LoggingConfig:
|
|
||||||
LogGroup: !Ref EventLog
|
|
||||||
Policies:
|
|
||||||
- DynamoDBCrudPolicy:
|
|
||||||
TableName: !Ref OrderTable
|
|
||||||
- DynamoDBCrudPolicy:
|
|
||||||
TableName: !Ref EnrollmentTable
|
|
||||||
- DynamoDBReadPolicy:
|
|
||||||
TableName: !Ref CourseTable
|
|
||||||
Events:
|
|
||||||
DynamoDBEvent:
|
|
||||||
Type: EventBridgeRule
|
|
||||||
Properties:
|
|
||||||
Pattern:
|
|
||||||
resources: [!Ref OrderTable]
|
|
||||||
detail-type: [INSERT]
|
|
||||||
detail:
|
|
||||||
new_image:
|
|
||||||
sk: [SEATS_ALLOCATION]
|
|
||||||
status: [PENDING]
|
|
||||||
|
|
||||||
SesPolicy:
|
SesPolicy:
|
||||||
Type: AWS::IAM::ManagedPolicy
|
Type: AWS::IAM::ManagedPolicy
|
||||||
Properties:
|
Properties:
|
||||||
@@ -395,8 +372,7 @@ Resources:
|
|||||||
Type: AWS::Serverless::Function
|
Type: AWS::Serverless::Function
|
||||||
Properties:
|
Properties:
|
||||||
Handler: events.issue_cert.lambda_handler
|
Handler: events.issue_cert.lambda_handler
|
||||||
Tracing: Active
|
Timeout: 12
|
||||||
Timeout: 10
|
|
||||||
LoggingConfig:
|
LoggingConfig:
|
||||||
LogGroup: !Ref EventLog
|
LogGroup: !Ref EventLog
|
||||||
Policies:
|
Policies:
|
||||||
@@ -417,6 +393,9 @@ Resources:
|
|||||||
sk: ['0']
|
sk: ['0']
|
||||||
new_image:
|
new_image:
|
||||||
status: [COMPLETED]
|
status: [COMPLETED]
|
||||||
|
cert:
|
||||||
|
issued_at:
|
||||||
|
- exists: false
|
||||||
old_image:
|
old_image:
|
||||||
status: [IN_PROGRESS]
|
status: [IN_PROGRESS]
|
||||||
|
|
||||||
@@ -424,11 +403,12 @@ Resources:
|
|||||||
Type: AWS::Serverless::Function
|
Type: AWS::Serverless::Function
|
||||||
Properties:
|
Properties:
|
||||||
Handler: events.ask_to_sign.lambda_handler
|
Handler: events.ask_to_sign.lambda_handler
|
||||||
Tracing: Active
|
|
||||||
Timeout: 12
|
Timeout: 12
|
||||||
Policies:
|
Policies:
|
||||||
- S3ReadPolicy:
|
- S3ReadPolicy:
|
||||||
BucketName: !Ref BucketName
|
BucketName: !Ref BucketName
|
||||||
|
- DynamoDBCrudPolicy:
|
||||||
|
TableName: !Ref EnrollmentTable
|
||||||
LoggingConfig:
|
LoggingConfig:
|
||||||
LogGroup: !Ref EventLog
|
LogGroup: !Ref EventLog
|
||||||
Events:
|
Events:
|
||||||
|
|||||||
818
enrollments-events/uv.lock
generated
818
enrollments-events/uv.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:104
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:106
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:layercake:104
|
- !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:layercake:106
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:104
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:106
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
|
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
@@ -30,6 +30,8 @@ dependencies = [
|
|||||||
"authlib>=1.6.5",
|
"authlib>=1.6.5",
|
||||||
"python-calamine>=0.5.4",
|
"python-calamine>=0.5.4",
|
||||||
"cloudflare>=4.3.1",
|
"cloudflare>=4.3.1",
|
||||||
|
"aws-durable-execution-sdk-python>=1.1.1",
|
||||||
|
"pydantic-core>=2.41.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ Resources:
|
|||||||
CompatibleArchitectures:
|
CompatibleArchitectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
CompatibleRuntimes:
|
CompatibleRuntimes:
|
||||||
- python3.12
|
|
||||||
- python3.13
|
- python3.13
|
||||||
|
- python3.14
|
||||||
RetentionPolicy: Delete
|
RetentionPolicy: Delete
|
||||||
Metadata:
|
Metadata:
|
||||||
BuildMethod: python3.13
|
BuildMethod: python3.14
|
||||||
BuildArchitecture: x86_64
|
BuildArchitecture: x86_64
|
||||||
|
|
||||||
Outputs:
|
Outputs:
|
||||||
|
|||||||
36
layercake/uv.lock
generated
36
layercake/uv.lock
generated
@@ -63,6 +63,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
|
{ url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aws-durable-execution-sdk-python"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "boto3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/63/9d/f3646d325d6c5ce3ad143d6e916d046ea94685c17ec6991331f6a233e187/aws_durable_execution_sdk_python-1.1.1.tar.gz", hash = "sha256:3812b60a72ab5c5fd9c1c1ffeca96260a9a79910b2c1fe4cb47c758b7768b1ce", size = 266978, upload-time = "2026-01-12T23:32:16.828Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/98/c7ff676db3306ac59733d971167c69a9d86fef0fa410eafde270b2f80380/aws_durable_execution_sdk_python-1.1.1-py3-none-any.whl", hash = "sha256:d724cb5e59ba1dbfce9228b527daac88a24001bc00bac9e3adb6f5b79a283f11", size = 89799, upload-time = "2026-01-12T23:32:15.057Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-encryption-sdk"
|
name = "aws-encryption-sdk"
|
||||||
version = "4.0.3"
|
version = "4.0.3"
|
||||||
@@ -140,16 +152,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "boto3"
|
name = "boto3"
|
||||||
version = "1.41.5"
|
version = "1.42.31"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "botocore" },
|
{ name = "botocore" },
|
||||||
{ name = "jmespath" },
|
{ name = "jmespath" },
|
||||||
{ name = "s3transfer" },
|
{ name = "s3transfer" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/81/450cd4143864959264a3d80f9246175a20de8c1e50ec889c710eaa28cdd9/boto3-1.41.5.tar.gz", hash = "sha256:bc7806bee681dfdff2fe2b74967b107a56274f1e66ebe4d20dc8eee1ea408d17", size = 111594, upload-time = "2025-11-26T20:27:47.021Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/a4/da/d81abc51bd35c2d2154f1caa0040843ada7df1689ea17d51c116c793b8f0/boto3-1.42.31.tar.gz", hash = "sha256:b2038fc5dbcd6746a16ada8d55fe73659b8cf95c7b6aeb63fe782e831485edaa", size = 112803, upload-time = "2026-01-20T21:04:45.292Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/56/f47a80254ed4991cce9a2f6d8ae8aafbc8df1c3270e966b2927289e5a12f/boto3-1.41.5-py3-none-any.whl", hash = "sha256:bb278111bfb4c33dca8342bda49c9db7685e43debbfa00cc2a5eb854dd54b745", size = 139344, upload-time = "2025-11-26T20:27:45.571Z" },
|
{ url = "https://files.pythonhosted.org/packages/fd/4d/2a2830424f11d575f7ca2abdff889138f212c7dd3f1dde2b2994a779a5ab/boto3-1.42.31-py3-none-any.whl", hash = "sha256:7f04b4cd7c375e4d88cc2cba3022c40805012ce8f57468b82cedb1bcd6b3a58a", size = 140572, upload-time = "2026-01-20T21:04:43.112Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -178,16 +190,16 @@ essential = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "botocore"
|
name = "botocore"
|
||||||
version = "1.41.5"
|
version = "1.42.31"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "jmespath" },
|
{ name = "jmespath" },
|
||||||
{ name = "python-dateutil" },
|
{ name = "python-dateutil" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/90/22/7fe08c726a2e3b11a0aef8bf177e83891c9cb2dc1809d35c9ed91a9e60e6/botocore-1.41.5.tar.gz", hash = "sha256:0367622b811597d183bfcaab4a350f0d3ede712031ce792ef183cabdee80d3bf", size = 14668152, upload-time = "2025-11-26T20:27:38.026Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ab/78/4fd91ed2f9d4b500680f33c714b7716fc37690083a8c8d3e94177cbc811e/botocore-1.42.31.tar.gz", hash = "sha256:62f2c31e229df625612dd4d7c72618948e4064436d71a647102f36fcddfa0f4d", size = 14895682, upload-time = "2026-01-20T21:04:32.999Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/4e/4e/21cd0b8f365449f1576f93de1ec8718ed18a7a3bc086dfbdeb79437bba7a/botocore-1.41.5-py3-none-any.whl", hash = "sha256:3fef7fcda30c82c27202d232cfdbd6782cb27f20f8e7e21b20606483e66ee73a", size = 14337008, upload-time = "2025-11-26T20:27:35.208Z" },
|
{ url = "https://files.pythonhosted.org/packages/9c/c3/6898ecbfc140754fc90702c43e63c2b13017cac345cd3015df404cfeb3e9/botocore-1.42.31-py3-none-any.whl", hash = "sha256:021346ad57cc3018acf4a46edc1f649b9818b33c07a08674ce1c36e9edbb5859", size = 14569714, upload-time = "2026-01-20T21:04:29.495Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -824,11 +836,12 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.12.0"
|
version = "0.13.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
{ name = "authlib" },
|
{ name = "authlib" },
|
||||||
|
{ name = "aws-durable-execution-sdk-python" },
|
||||||
{ name = "aws-lambda-powertools", extra = ["all"] },
|
{ name = "aws-lambda-powertools", extra = ["all"] },
|
||||||
{ name = "cloudflare" },
|
{ name = "cloudflare" },
|
||||||
{ name = "dictdiffer" },
|
{ name = "dictdiffer" },
|
||||||
@@ -841,6 +854,7 @@ dependencies = [
|
|||||||
{ name = "psycopg", extra = ["binary"] },
|
{ name = "psycopg", extra = ["binary"] },
|
||||||
{ name = "pycpfcnpj" },
|
{ name = "pycpfcnpj" },
|
||||||
{ name = "pydantic", extra = ["email"] },
|
{ name = "pydantic", extra = ["email"] },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
{ name = "pydantic-extra-types" },
|
{ name = "pydantic-extra-types" },
|
||||||
{ name = "python-calamine" },
|
{ name = "python-calamine" },
|
||||||
{ name = "python-multipart" },
|
{ name = "python-multipart" },
|
||||||
@@ -866,6 +880,7 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "arnparse", specifier = ">=0.0.2" },
|
{ name = "arnparse", specifier = ">=0.0.2" },
|
||||||
{ name = "authlib", specifier = ">=1.6.5" },
|
{ name = "authlib", specifier = ">=1.6.5" },
|
||||||
|
{ name = "aws-durable-execution-sdk-python", specifier = ">=1.1.1" },
|
||||||
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.23.0" },
|
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.23.0" },
|
||||||
{ name = "cloudflare", specifier = ">=4.3.1" },
|
{ name = "cloudflare", specifier = ">=4.3.1" },
|
||||||
{ name = "dictdiffer", specifier = ">=0.9.0" },
|
{ name = "dictdiffer", specifier = ">=0.9.0" },
|
||||||
@@ -878,6 +893,7 @@ requires-dist = [
|
|||||||
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" },
|
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" },
|
||||||
{ name = "pycpfcnpj", specifier = ">=1.8" },
|
{ name = "pycpfcnpj", specifier = ">=1.8" },
|
||||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
|
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
|
||||||
|
{ name = "pydantic-core", specifier = ">=2.41.5" },
|
||||||
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
|
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
|
||||||
{ name = "python-calamine", specifier = ">=0.5.4" },
|
{ name = "python-calamine", specifier = ">=0.5.4" },
|
||||||
{ name = "python-multipart", specifier = ">=0.0.20" },
|
{ name = "python-multipart", specifier = ">=0.0.20" },
|
||||||
@@ -1935,14 +1951,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "s3transfer"
|
name = "s3transfer"
|
||||||
version = "0.15.0"
|
version = "0.16.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "botocore" },
|
{ name = "botocore" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/bb/940d6af975948c1cc18f44545ffb219d3c35d78ec972b42ae229e8e37e08/s3transfer-0.15.0.tar.gz", hash = "sha256:d36fac8d0e3603eff9b5bfa4282c7ce6feb0301a633566153cbd0b93d11d8379", size = 152185, upload-time = "2025-11-20T20:28:56.327Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/e1/5ef25f52973aa12a19cf4e1375d00932d7fb354ffd310487ba7d44225c1a/s3transfer-0.15.0-py3-none-any.whl", hash = "sha256:6f8bf5caa31a0865c4081186689db1b2534cef721d104eb26101de4b9d6a5852", size = 85984, upload-time = "2025-11-20T20:28:55.046Z" },
|
{ url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from aws_lambda_powertools.logging import correlation_paths
|
|||||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||||
from layercake.dateutils import now
|
from layercake.dateutils import now
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||||
|
from layercake.funcs import pick
|
||||||
|
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import ORDER_TABLE
|
from config import ORDER_TABLE
|
||||||
@@ -29,10 +30,12 @@ class OrderNotFoundError(NotFoundError): ...
|
|||||||
class InvoiceNotFoundError(NotFoundError): ...
|
class InvoiceNotFoundError(NotFoundError): ...
|
||||||
|
|
||||||
|
|
||||||
class StatusAttr(Enum):
|
class StatusTimestampAttr(Enum):
|
||||||
# Post-migration (orders): uncomment the following lines
|
# Post-migration (orders): uncomment the following 2 lines
|
||||||
# PAID = 'paid_at'
|
# PAID = 'paid_at'
|
||||||
# EXTERNALLY_PAID = 'paid_at'
|
# EXTERNALLY_PAID = 'paid_at'
|
||||||
|
|
||||||
|
# Post-migration (orders): remove the following 2 lines
|
||||||
EXTERNALLY_PAID = 'payment_date'
|
EXTERNALLY_PAID = 'payment_date'
|
||||||
PAID = 'payment_date'
|
PAID = 'payment_date'
|
||||||
|
|
||||||
@@ -41,19 +44,24 @@ class StatusAttr(Enum):
|
|||||||
EXPIRED = 'expired_at'
|
EXPIRED = 'expired_at'
|
||||||
|
|
||||||
|
|
||||||
def _status_attr(status: str) -> StatusAttr | None:
|
def _timestamp_attr_for_status(status: str) -> str | None:
|
||||||
try:
|
try:
|
||||||
return StatusAttr[status]
|
return StatusTimestampAttr[status].value
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _friendly_status(status: str) -> str:
|
def _friendly_status(status: str) -> str:
|
||||||
if 'status' == 'EXTERNALLY_PAID':
|
if status == 'EXTERNALLY_PAID':
|
||||||
return 'PAID'
|
return 'PAID'
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
def _get_order_owner(order_id: str) -> dict:
|
||||||
|
r = dyn.get_item(KeyPair(order_id, '0'))
|
||||||
|
return pick(('user_id', 'org_id'), r)
|
||||||
|
|
||||||
|
|
||||||
@app.post('/<order_id>/postback')
|
@app.post('/<order_id>/postback')
|
||||||
@tracer.capture_method
|
@tracer.capture_method
|
||||||
def postback(order_id: str):
|
def postback(order_id: str):
|
||||||
@@ -62,31 +70,33 @@ def postback(order_id: str):
|
|||||||
|
|
||||||
now_ = now()
|
now_ = now()
|
||||||
event = decoded_body['event']
|
event = decoded_body['event']
|
||||||
status = decoded_body.get('data[status]', '').upper()
|
raw_status = decoded_body.get('data[status]', '').upper()
|
||||||
status_attr = _status_attr(status)
|
status = _friendly_status(raw_status)
|
||||||
|
timestamp_attr = _timestamp_attr_for_status(raw_status)
|
||||||
|
|
||||||
if event != 'invoice.status_changed' or not status_attr:
|
if event != 'invoice.status_changed' or not timestamp_attr:
|
||||||
return Response(status_code=HTTPStatus.NO_CONTENT)
|
logger.debug('Event not acceptable', order_id=order_id)
|
||||||
|
return Response(status_code=HTTPStatus.NOT_ACCEPTABLE)
|
||||||
|
|
||||||
with dyn.transact_writer() as transact:
|
with dyn.transact_writer() as transact:
|
||||||
transact.update(
|
transact.update(
|
||||||
key=KeyPair(order_id, '0'),
|
key=KeyPair(order_id, '0'),
|
||||||
update_expr='SET #status = :status, \
|
update_expr='SET #status = :status, \
|
||||||
#status_attr = :now, \
|
#ts_attr = :now, \
|
||||||
updated_at = :now',
|
updated_at = :now',
|
||||||
cond_expr='attribute_exists(sk)',
|
cond_expr='attribute_exists(sk)',
|
||||||
expr_attr_names={
|
expr_attr_names={
|
||||||
'#status': 'status',
|
'#status': 'status',
|
||||||
'#status_attr': status_attr.value,
|
'#ts_attr': timestamp_attr,
|
||||||
},
|
},
|
||||||
expr_attr_values={
|
expr_attr_values={
|
||||||
':status': _friendly_status(status),
|
':status': status,
|
||||||
':now': now_,
|
':now': now_,
|
||||||
},
|
},
|
||||||
exc_cls=OrderNotFoundError,
|
exc_cls=OrderNotFoundError,
|
||||||
)
|
)
|
||||||
|
|
||||||
if status == 'EXTERNALLY_PAID':
|
if raw_status == 'EXTERNALLY_PAID':
|
||||||
transact.update(
|
transact.update(
|
||||||
key=KeyPair(order_id, 'INVOICE'),
|
key=KeyPair(order_id, 'INVOICE'),
|
||||||
cond_expr='attribute_exists(sk)',
|
cond_expr='attribute_exists(sk)',
|
||||||
@@ -99,6 +109,51 @@ def postback(order_id: str):
|
|||||||
exc_cls=InvoiceNotFoundError,
|
exc_cls=InvoiceNotFoundError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if status == 'PAID':
|
||||||
|
try:
|
||||||
|
dyn.put_item(
|
||||||
|
item={
|
||||||
|
'id': order_id,
|
||||||
|
'sk': 'FULFILLMENT',
|
||||||
|
'status': 'IN_PROGRESS',
|
||||||
|
'created_at': now_,
|
||||||
|
**_get_order_owner(order_id),
|
||||||
|
},
|
||||||
|
cond_expr='attribute_not_exists(sk)',
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if status in ('CANCELED', 'REFUNDED'):
|
||||||
|
try:
|
||||||
|
with dyn.transact_writer() as transact:
|
||||||
|
transact.condition(
|
||||||
|
key=KeyPair(order_id, 'FULFILLMENT'),
|
||||||
|
cond_expr=(
|
||||||
|
'attribute_exists(sk) '
|
||||||
|
'AND #status <> :in_progress '
|
||||||
|
'AND #status <> :rollback'
|
||||||
|
),
|
||||||
|
expr_attr_names={
|
||||||
|
'#status': 'status',
|
||||||
|
},
|
||||||
|
expr_attr_values={
|
||||||
|
':in_progress': 'IN_PROGRESS',
|
||||||
|
':rollback': 'ROLLBACK',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
transact.put(
|
||||||
|
item={
|
||||||
|
'id': order_id,
|
||||||
|
'sk': 'FULFILLMENT#ROLLBACK',
|
||||||
|
'created_at': now_,
|
||||||
|
},
|
||||||
|
cond_expr='attribute_not_exists(sk)',
|
||||||
|
)
|
||||||
|
logger.debug('Fulfillment rollback event created', order_id=order_id)
|
||||||
|
except Exception:
|
||||||
|
logger.debug('Fulfillment rollback event already exists', order_id=order_id)
|
||||||
|
|
||||||
return Response(status_code=HTTPStatus.NO_CONTENT)
|
return Response(status_code=HTTPStatus.NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
item={
|
item={
|
||||||
'id': pk,
|
'id': pk,
|
||||||
'sk': f'{sk}#SCHEDULE#AUTO_CLOSE',
|
'sk': f'{sk}#SCHEDULE#AUTO_CLOSE',
|
||||||
|
# Post-migration: uncomment the following line
|
||||||
|
# 'sk': f'{sk}#SCHEDULED#AUTO_CLOSE',
|
||||||
'ttl': ttl(
|
'ttl': ttl(
|
||||||
start_dt=datetime.combine(end_period, time())
|
start_dt=datetime.combine(end_period, time())
|
||||||
+ timedelta(days=1)
|
+ timedelta(days=1)
|
||||||
|
|||||||
@@ -80,10 +80,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
cond_expr='attribute_not_exists(sk)',
|
cond_expr='attribute_not_exists(sk)',
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception(
|
logger.exception(exc, keypair={'id': pk, 'sk': sk})
|
||||||
exc,
|
|
||||||
keypair={'id': pk, 'sk': sk},
|
|
||||||
)
|
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -107,8 +107,8 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
'sk': 'SCHEDULE#SELF_DESTRUCTION',
|
'sk': 'SCHEDULED#SELF_DESTRUCTION',
|
||||||
'ttl': ttl(start_dt=now_, days=14),
|
'ttl': ttl(start_dt=now_, days=7),
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
order_id = keys['id']
|
order_id = keys['id']
|
||||||
|
|
||||||
r = dyn.collection.query(PartitionKey(order_id), limit=150)
|
r = dyn.collection.query(PartitionKey(order_id), limit=150)
|
||||||
logger.info('Records found', total_items=len(r['items']), records=r['items'])
|
logger.info(
|
||||||
|
msg='Records found',
|
||||||
|
total_items=len(r['items']),
|
||||||
|
records=r['items'],
|
||||||
|
)
|
||||||
|
|
||||||
with dyn.batch_writer() as batch:
|
with dyn.batch_writer() as batch:
|
||||||
for pair in r['items']:
|
for pair in r['items']:
|
||||||
|
|||||||
78
orders-events/app/events/start_fulfillment.py
Normal file
78
orders-events/app/events/start_fulfillment.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import pprint
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from aws_lambda_powertools import Logger
|
||||||
|
from aws_lambda_powertools.utilities.data_classes import (
|
||||||
|
EventBridgeEvent,
|
||||||
|
event_source,
|
||||||
|
)
|
||||||
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||||
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyChain, KeyPair
|
||||||
|
|
||||||
|
from boto3clients import dynamodb_client
|
||||||
|
from config import ENROLLMENT_TABLE
|
||||||
|
|
||||||
|
logger = Logger(__name__)
|
||||||
|
dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||||
|
|
||||||
|
|
||||||
|
@event_source(data_class=EventBridgeEvent)
|
||||||
|
@logger.inject_lambda_context
|
||||||
|
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||||
|
new_image = event.detail['new_image']
|
||||||
|
order_id = new_image['id']
|
||||||
|
enrollments = dyn.collection.query(
|
||||||
|
key=KeyPair(order_id, 'ENROLLMENT#'),
|
||||||
|
).get('items', [])
|
||||||
|
|
||||||
|
if not enrollments:
|
||||||
|
items = dyn.collection.get_item(
|
||||||
|
KeyPair(order_id, 'ITEMS'),
|
||||||
|
raise_on_error=False,
|
||||||
|
default=[],
|
||||||
|
)
|
||||||
|
pprint.pp(items)
|
||||||
|
# docx = {
|
||||||
|
# 'id': f'SEAT#ORG#{org_id}',
|
||||||
|
# 'sk': f'ORDER#{order_id}#ENROLLMENT#{uuid4()}',
|
||||||
|
# 'course': {},
|
||||||
|
# 'created_at': now_,
|
||||||
|
# }
|
||||||
|
|
||||||
|
pprint.pp(enrollments)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# Se houver matriculas
|
||||||
|
# -> com: scheduled_for
|
||||||
|
# -> tenta agendar, se não joga para vagas
|
||||||
|
# -> tenta matriculas, se falhar, joga para vagas
|
||||||
|
|
||||||
|
# se não houver vagas, gera as vagas.
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Course:
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
access_period: int
|
||||||
|
|
||||||
|
|
||||||
|
def _get_courses(ids: set) -> tuple[Course, ...]:
|
||||||
|
pairs = tuple(KeyPair(idx, '0') for idx in ids)
|
||||||
|
r = dyn.collection.get_items(
|
||||||
|
KeyChain(pairs),
|
||||||
|
flatten_top=False,
|
||||||
|
)
|
||||||
|
courses = tuple(
|
||||||
|
Course(
|
||||||
|
id=idx,
|
||||||
|
name=obj['name'],
|
||||||
|
access_period=obj['access_period'],
|
||||||
|
)
|
||||||
|
for idx, obj in r.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
return courses
|
||||||
@@ -17,7 +17,7 @@ dev = [
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
pythonpath = ["app/"]
|
pythonpath = ["app/"]
|
||||||
addopts = "--cov --cov-report html -v"
|
addopts = "--cov=app --cov-report html"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py311"
|
target-version = "py311"
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ Parameters:
|
|||||||
Globals:
|
Globals:
|
||||||
Function:
|
Function:
|
||||||
CodeUri: app/
|
CodeUri: app/
|
||||||
Runtime: python3.13
|
Runtime: python3.14
|
||||||
Tracing: Active
|
Tracing: Active
|
||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:layercake:104
|
- !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:layercake:106
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
@@ -189,7 +189,7 @@ Resources:
|
|||||||
Type: AWS::Serverless::Function
|
Type: AWS::Serverless::Function
|
||||||
Properties:
|
Properties:
|
||||||
Handler: events.billing.close_window.lambda_handler
|
Handler: events.billing.close_window.lambda_handler
|
||||||
Timeout: 12
|
Timeout: 30
|
||||||
LoggingConfig:
|
LoggingConfig:
|
||||||
LogGroup: !Ref EventLog
|
LogGroup: !Ref EventLog
|
||||||
Policies:
|
Policies:
|
||||||
@@ -209,6 +209,8 @@ Resources:
|
|||||||
id:
|
id:
|
||||||
- prefix: BILLING
|
- prefix: BILLING
|
||||||
sk:
|
sk:
|
||||||
|
# Post-migration: uncomment the following line
|
||||||
|
# - suffix: SCHEDULED#AUTO_CLOSE
|
||||||
- suffix: SCHEDULE#AUTO_CLOSE
|
- suffix: SCHEDULE#AUTO_CLOSE
|
||||||
|
|
||||||
EventBillingSendEmailOnClosingFunction:
|
EventBillingSendEmailOnClosingFunction:
|
||||||
@@ -247,10 +249,39 @@ Resources:
|
|||||||
old_image:
|
old_image:
|
||||||
status: [PENDING]
|
status: [PENDING]
|
||||||
|
|
||||||
|
EventStartFulfillmentFunction:
|
||||||
|
Type: AWS::Serverless::Function
|
||||||
|
Properties:
|
||||||
|
Handler: events.start_fulfillment.lambda_handler
|
||||||
|
Timeout: 30
|
||||||
|
LoggingConfig:
|
||||||
|
LogGroup: !Ref EventLog
|
||||||
|
Policies:
|
||||||
|
- DynamoDBCrudPolicy:
|
||||||
|
TableName: !Ref OrderTable
|
||||||
|
- DynamoDBCrudPolicy:
|
||||||
|
TableName: !Ref EnrollmentTable
|
||||||
|
- DynamoDBReadPolicy:
|
||||||
|
TableName: !Ref CourseTable
|
||||||
|
Events:
|
||||||
|
DynamoDBEvent:
|
||||||
|
Type: EventBridgeRule
|
||||||
|
Properties:
|
||||||
|
Pattern:
|
||||||
|
resources: [!Ref OrderTable]
|
||||||
|
detail-type: [INSERT]
|
||||||
|
detail:
|
||||||
|
new_image:
|
||||||
|
sk: [FULFILLMENT]
|
||||||
|
status: [IN_PROGRESS]
|
||||||
|
org_id:
|
||||||
|
- exists: true
|
||||||
|
|
||||||
EventRunSelfDestructionFunction:
|
EventRunSelfDestructionFunction:
|
||||||
Type: AWS::Serverless::Function
|
Type: AWS::Serverless::Function
|
||||||
Properties:
|
Properties:
|
||||||
Handler: events.run_self_destruction.lambda_handler
|
Handler: events.run_self_destruction.lambda_handler
|
||||||
|
Timeout: 30
|
||||||
LoggingConfig:
|
LoggingConfig:
|
||||||
LogGroup: !Ref EventLog
|
LogGroup: !Ref EventLog
|
||||||
Policies:
|
Policies:
|
||||||
@@ -265,7 +296,7 @@ Resources:
|
|||||||
detail-type: [EXPIRE]
|
detail-type: [EXPIRE]
|
||||||
detail:
|
detail:
|
||||||
keys:
|
keys:
|
||||||
sk: ['SCHEDULE#SELF_DESTRUCTION']
|
sk: ['SCHEDULED#SELF_DESTRUCTION']
|
||||||
|
|
||||||
# DEPRECATED
|
# DEPRECATED
|
||||||
EventAppendOrgIdFunction:
|
EventAppendOrgIdFunction:
|
||||||
@@ -322,6 +353,7 @@ Resources:
|
|||||||
user_id:
|
user_id:
|
||||||
- exists: false
|
- exists: false
|
||||||
|
|
||||||
|
# DEPRECATED
|
||||||
EventRemoveSlotsIfCanceledFunction:
|
EventRemoveSlotsIfCanceledFunction:
|
||||||
Type: AWS::Serverless::Function
|
Type: AWS::Serverless::Function
|
||||||
Properties:
|
Properties:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from decimal import Decimal
|
||||||
from http import HTTPMethod
|
from http import HTTPMethod
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
@@ -155,7 +156,10 @@ def dynamodb_persistence_layer(dynamodb_client):
|
|||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def dynamodb_seeds(dynamodb_persistence_layer):
|
def dynamodb_seeds(dynamodb_persistence_layer):
|
||||||
with open('tests/seeds.jsonl', 'rb') as fp:
|
with open('tests/seeds.jsonl', 'rb') as fp:
|
||||||
reader = jsonlines.Reader(fp)
|
reader = jsonlines.Reader(
|
||||||
|
fp,
|
||||||
|
loads=lambda s: json.loads(s, parse_float=Decimal), # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
for line in reader.iter(type=dict, skip_invalid=True):
|
for line in reader.iter(type=dict, skip_invalid=True):
|
||||||
dynamodb_persistence_layer.put_item(item=line)
|
dynamodb_persistence_layer.put_item(item=line)
|
||||||
|
|||||||
42
orders-events/tests/events/test_start_fulfillment.py
Normal file
42
orders-events/tests/events/test_start_fulfillment.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||||
|
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||||
|
|
||||||
|
import events.start_fulfillment as app
|
||||||
|
|
||||||
|
|
||||||
|
def test_fulfillment_enrollments(
|
||||||
|
dynamodb_seeds,
|
||||||
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
|
lambda_context: LambdaContext,
|
||||||
|
):
|
||||||
|
event = {
|
||||||
|
'detail': {
|
||||||
|
'new_image': {
|
||||||
|
'id': '9b9441d2-4ae3-4b50-8cb6-71e872d4492a',
|
||||||
|
'sk': 'FULFILLMENT',
|
||||||
|
'org_id': 'cJtK9SsnJhKPyxESe7g3DG',
|
||||||
|
'status': 'IN_PROGRESS',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def test_fulfillment_items(
|
||||||
|
dynamodb_seeds,
|
||||||
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
|
lambda_context: LambdaContext,
|
||||||
|
):
|
||||||
|
event = {
|
||||||
|
'detail': {
|
||||||
|
'new_image': {
|
||||||
|
'id': '9f7fa055-7c0b-418a-b023-77477d1895b9',
|
||||||
|
'sk': 'FULFILLMENT',
|
||||||
|
'org_id': 'cJtK9SsnJhKPyxESe7g3DG',
|
||||||
|
'status': 'IN_PROGRESS',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||||
@@ -10,9 +10,18 @@
|
|||||||
{"id": "6a60d026-d383-4707-b093-b6eddea1a24e", "sk": "ITEMS", "items": [{"id": "a810dd22-56c0-4d9b-8cd2-7e2ee9c45839", "name": "pytest", "quantity": 1, "unit_price": 109}]}
|
{"id": "6a60d026-d383-4707-b093-b6eddea1a24e", "sk": "ITEMS", "items": [{"id": "a810dd22-56c0-4d9b-8cd2-7e2ee9c45839", "name": "pytest", "quantity": 1, "unit_price": 109}]}
|
||||||
{"id": "a810dd22-56c0-4d9b-8cd2-7e2ee9c45839", "sk": "metadata#betaeducacao", "course_id": "dc1a0428-47bf-4db1-a5da-24be49c9fda6", "create_date": "2025-06-05T12:13:54.371416+00:00"}
|
{"id": "a810dd22-56c0-4d9b-8cd2-7e2ee9c45839", "sk": "metadata#betaeducacao", "course_id": "dc1a0428-47bf-4db1-a5da-24be49c9fda6", "create_date": "2025-06-05T12:13:54.371416+00:00"}
|
||||||
|
|
||||||
{"id": "2849f1d5-f4f1-411e-8497-ec3a40afc0ab", "sk": "0" "payment_method": "BANK_SLIP", "status": "PENDING", "total": 178.2, "due_date": "", "email": "org+15608435000190@users.noreply.saladeaula.digital", "name": "Beta Educação", "coupon": "10OFF", "discount": -19.8, "create_date": "2026-01-07T19:09:54.193859-03:00", "updated_at": "2026-01-07T19:09:54.871374-03:00", "org_id": "cJtK9SsnJhKPyxESe7g3DG", "subtotal": 198, "tenant_id": "cJtK9SsnJhKPyxESe7g3DG", "cnpj": "15608435000190"}
|
{"id": "2849f1d5-f4f1-411e-8497-ec3a40afc0ab", "sk": "0", "payment_method": "BANK_SLIP", "status": "PENDING", "total": 178.2, "due_date": "", "email": "org+15608435000190@users.noreply.saladeaula.digital", "name": "Beta Educação", "coupon": "10OFF", "discount": -19.8, "create_date": "2026-01-07T19:09:54.193859-03:00", "updated_at": "2026-01-07T19:09:54.871374-03:00", "org_id": "cJtK9SsnJhKPyxESe7g3DG", "subtotal": 198, "tenant_id": "cJtK9SsnJhKPyxESe7g3DG", "cnpj": "15608435000190"}
|
||||||
{"id": "2849f1d5-f4f1-411e-8497-ec3a40afc0ab", "sk": "ITEMS", "items": [ { "name": "CIPA Grau de Risco 1", "id": "3c27ea9c-9464-46a1-9717-8c1441793186", "quantity": 1, "unit_price": 99 }, { "name": "CIPA Grau de Risco 2", "id": "99bb3b60-4ded-4a8e-937c-ba2d78ec6454", "quantity": 1, "unit_price": 99 } ], "created_at": "2026-01-07T19:09:54.193859-03:00"}
|
{"id": "2849f1d5-f4f1-411e-8497-ec3a40afc0ab", "sk": "ITEMS", "items": [ { "name": "CIPA Grau de Risco 1", "id": "3c27ea9c-9464-46a1-9717-8c1441793186", "quantity": 1, "unit_price": 99 }, { "name": "CIPA Grau de Risco 2", "id": "99bb3b60-4ded-4a8e-937c-ba2d78ec6454", "quantity": 1, "unit_price": 99 } ], "created_at": "2026-01-07T19:09:54.193859-03:00"}
|
||||||
{"id": "2849f1d5-f4f1-411e-8497-ec3a40afc0ab", "sk": "ADDRESS", "city": "São José", "postcode": "88101001", "state": "SC", "created_at": "2026-01-07T19:09:54.193859-03:00", "address1": "Avenida Presidente Kennedy" "address2": "", "neighborhood": "Campinas"}
|
{"id": "2849f1d5-f4f1-411e-8497-ec3a40afc0ab", "sk": "ADDRESS", "city": "São José", "postcode": "88101001", "state": "SC", "created_at": "2026-01-07T19:09:54.193859-03:00", "address1": "Avenida Presidente Kennedy", "address2": "", "neighborhood": "Campinas"}
|
||||||
|
|
||||||
|
// Seeds for Order
|
||||||
|
// file: tests/events/test_start_fulfillment.py
|
||||||
|
{"id": "9b9441d2-4ae3-4b50-8cb6-71e872d4492a", "sk": "ITEMS", "items": [ { "name": "Combate a Incêndio", "id": "4866c068-577a-45b0-b41a-41a7dc6b9ab7", "quantity": 2, "unit_price": 99 } ], "created_at": "2026-01-20T13:05:52.737256-03:00"}
|
||||||
|
{"id": "9b9441d2-4ae3-4b50-8cb6-71e872d4492a", "sk": "ENROLLMENT#7d3f5457-8533-4f27-a0a4-ffa209a93f7d", "course": { "name": "Combate a Incêndio", "id": "4866c068-577a-45b0-b41a-41a7dc6b9ab7", "access_period": 365 }, "user": { "name": "Maitê L Siqueira", "cpf": "02186829991", "id": "87606a7f-de56-4198-a91d-b6967499d382", "email": "osergiosiqueira+maite@gmail.com" }, "created_at": "2026-01-20T13:05:52.737256-03:00", "status": "PENDING"}
|
||||||
|
{"id": "9b9441d2-4ae3-4b50-8cb6-71e872d4492a", "sk": "ENROLLMENT#9576855e-b259-4f3e-8315-1612a5cb8c36", "user": { "name": "Sérgio Rafael de Siqueira", "cpf": "07879819908", "id": "5OxmMjL-ujoR5IMGegQz", "email": "sergio@somosbeta.com.br" }, "course": { "name": "Combate a Incêndio", "id": "4866c068-577a-45b0-b41a-41a7dc6b9ab7", "access_period": 365 }, "created_at": "2026-01-20T13:05:52.737256-03:00", "status": "PENDING"}
|
||||||
|
// Seeds for Order
|
||||||
|
{"id": "9f7fa055-7c0b-418a-b023-77477d1895b9", "sk": "ITEMS", "items": [ { "name": "Combate a Incêndio", "id": "4866c068-577a-45b0-b41a-41a7dc6b9ab7", "quantity": 2, "unit_price": 99 } ], "created_at": "2026-01-20T13:05:52.737256-03:00"}
|
||||||
|
|
||||||
|
|
||||||
// Seeds for Iugu
|
// Seeds for Iugu
|
||||||
// file: tests/test_app.py
|
// file: tests/test_app.py
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -36,6 +36,11 @@ def test_postback(
|
|||||||
)
|
)
|
||||||
assert r['statusCode'] == HTTPStatus.NO_CONTENT
|
assert r['statusCode'] == HTTPStatus.NO_CONTENT
|
||||||
|
|
||||||
order = dynamodb_persistence_layer.get_item(KeyPair(order_id, '0'))
|
order = dynamodb_persistence_layer.collection.get_items(
|
||||||
|
TransactKey(order_id)
|
||||||
|
+ SortKey('0')
|
||||||
|
+ SortKey('FULFILLMENT', rename_key='fulfillment')
|
||||||
|
)
|
||||||
|
assert 'fulfillment' in order
|
||||||
|
|
||||||
assert order['status'] == 'PAID'
|
assert order['status'] == 'PAID'
|
||||||
|
|||||||
18
orders-events/uv.lock
generated
18
orders-events/uv.lock
generated
@@ -62,6 +62,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aws-durable-execution-sdk-python"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "boto3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/63/9d/f3646d325d6c5ce3ad143d6e916d046ea94685c17ec6991331f6a233e187/aws_durable_execution_sdk_python-1.1.1.tar.gz", hash = "sha256:3812b60a72ab5c5fd9c1c1ffeca96260a9a79910b2c1fe4cb47c758b7768b1ce", size = 266978, upload-time = "2026-01-12T23:32:16.828Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/98/c7ff676db3306ac59733d971167c69a9d86fef0fa410eafde270b2f80380/aws_durable_execution_sdk_python-1.1.1-py3-none-any.whl", hash = "sha256:d724cb5e59ba1dbfce9228b527daac88a24001bc00bac9e3adb6f5b79a283f11", size = 89799, upload-time = "2026-01-12T23:32:15.057Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-encryption-sdk"
|
name = "aws-encryption-sdk"
|
||||||
version = "4.0.3"
|
version = "4.0.3"
|
||||||
@@ -746,11 +758,12 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.12.0"
|
version = "0.13.1"
|
||||||
source = { directory = "../layercake" }
|
source = { directory = "../layercake" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
{ name = "authlib" },
|
{ name = "authlib" },
|
||||||
|
{ name = "aws-durable-execution-sdk-python" },
|
||||||
{ name = "aws-lambda-powertools", extra = ["all"] },
|
{ name = "aws-lambda-powertools", extra = ["all"] },
|
||||||
{ name = "cloudflare" },
|
{ name = "cloudflare" },
|
||||||
{ name = "dictdiffer" },
|
{ name = "dictdiffer" },
|
||||||
@@ -763,6 +776,7 @@ dependencies = [
|
|||||||
{ name = "psycopg", extra = ["binary"] },
|
{ name = "psycopg", extra = ["binary"] },
|
||||||
{ name = "pycpfcnpj" },
|
{ name = "pycpfcnpj" },
|
||||||
{ name = "pydantic", extra = ["email"] },
|
{ name = "pydantic", extra = ["email"] },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
{ name = "pydantic-extra-types" },
|
{ name = "pydantic-extra-types" },
|
||||||
{ name = "python-calamine" },
|
{ name = "python-calamine" },
|
||||||
{ name = "python-multipart" },
|
{ name = "python-multipart" },
|
||||||
@@ -777,6 +791,7 @@ dependencies = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "arnparse", specifier = ">=0.0.2" },
|
{ name = "arnparse", specifier = ">=0.0.2" },
|
||||||
{ name = "authlib", specifier = ">=1.6.5" },
|
{ name = "authlib", specifier = ">=1.6.5" },
|
||||||
|
{ name = "aws-durable-execution-sdk-python", specifier = ">=1.1.1" },
|
||||||
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.23.0" },
|
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.23.0" },
|
||||||
{ name = "cloudflare", specifier = ">=4.3.1" },
|
{ name = "cloudflare", specifier = ">=4.3.1" },
|
||||||
{ name = "dictdiffer", specifier = ">=0.9.0" },
|
{ name = "dictdiffer", specifier = ">=0.9.0" },
|
||||||
@@ -789,6 +804,7 @@ requires-dist = [
|
|||||||
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" },
|
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" },
|
||||||
{ name = "pycpfcnpj", specifier = ">=1.8" },
|
{ name = "pycpfcnpj", specifier = ">=1.8" },
|
||||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
|
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
|
||||||
|
{ name = "pydantic-core", specifier = ">=2.41.5" },
|
||||||
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
|
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
|
||||||
{ name = "python-calamine", specifier = ">=0.5.4" },
|
{ name = "python-calamine", specifier = ">=0.5.4" },
|
||||||
{ name = "python-multipart", specifier = ">=0.0.20" },
|
{ name = "python-multipart", specifier = ">=0.0.20" },
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:layercake:104
|
- !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:layercake:106
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
LOG_LEVEL: DEBUG
|
LOG_LEVEL: DEBUG
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:layercake:104
|
- !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:layercake:106
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
Reference in New Issue
Block a user