src/checkout/checkout.oxl
use super.model.*
use super.orders { Orders, Order }
use super.cart { Cart }
use super.limits { CHECKOUT_DEADLINE }
use super.photos { uploadPhoto as photoPipeline, PhotoId, BadImage }
use super.search { searchCatalog }
// Внешние системы описаны в .proto; из них генерируются extern service и типы.
// reserve/release/charge/refund помечены в proto как идемпотентные — это нужно саге.
// Сообщение-запрос `import proto` разворачивается в параметры rpc (D-4742, Стандарт V.5): `reserve(items: …)`.
// ОТКЛОНЕНИЕ (DEVIATIONS.md #1): позиции склада — `Line` из proto, а не `Item` из model.
import proto "proto/inventory.proto"
import proto "proto/payments.proto"
import proto "proto/recs.proto"
// Типы из .proto приватны модулю; тесты берут их под этими именами
pub type OutOfStockError = OutOfStock
pub type DeclinedError = Declined
// Клиенты внешних сервисов; создаются через provide в cluster.oxl только на роли saga (Recs — на api)
dep inventory: Client<Inventory>
dep payments: Client<Payments>
// Сага: шаги идут по очереди; если шаг упал, уже выполненные откатываются компенсациями (undo) в обратном порядке.
// Прогресс пишется в журнал, поэтому после падения узла сагу продолжит другой узел роли saga.
saga placeOrder(snap: CartSnapshot) -> Order {
inventory.reserve(items: snap.items.map(i => Line{ sku: "{i.sku}", qty: i.qty })) // 1. резервируем товары
undo r => inventory.release(r.reservationId) // откат: снять резерв
-> payments.charge(user: snap.userId.raw, amount: snap.total.minor) // 2. списываем деньги
undo p => payments.refund(p.receiptId) // откат: вернуть деньги
-> Orders.create(snap, it.receiptId) // 3. создаём заказ; it — результат шага 2
}
// Публичный фасад для клиентов
@v(1) pub(api) service Shop {
rpc addToCart(item: Item) -> () | CartFull
rpc checkout() -> Order | EmptyCart | OutOfStock | Declined | CompensationFailed
rpc uploadPhoto(img: bytes) -> PhotoId | BadImage
rpc search(q: str) -> List<str> // ERRATUM V.5 (DEVIATIONS.md #13): поиск с A/B-экспериментом «search v2» (§18.5)
}
impl Shop {
dep recs: Client<Recs> // ОТКЛОНЕНИЕ (DEVIATIONS.md #1): модульный `dep` доступен только в сагах и пайплайнах (D-5101), в rpc — `dep` impl
// Cart(uid) — ссылка на шардированный актор; сообщение уходит на узел, где живёт корзина (V.5)
rpc addToCart(item) { Cart(callerId(ctx)?).add(item) }
rpc checkout() {
let uid = callerId(ctx)?
let res = await Cart(uid).snapshot()
-> s => if isEmpty(&s) { EmptyCart{} } else { s } // пустая корзина — ошибка значением, цепочка прерывается
-> placeOrder(it) // запускаем сагу
within CHECKOUT_DEADLINE // общий дедлайн оформления
let order = res?
// Очистку нельзя отправлять в фон: она могла бы прийти после следующего запроса пользователя и стереть только что
// добавленное. Поэтому ждём; убираются лишь заказанные позиции.
Cart(uid).removeOrdered(order.items.len())?
// В фон выносится только то, что не влияет на состояние запроса: данные заказа для дообучения рекомендаций
await order ~> recs.learn(user: uid.raw, items: it.items.map(i => LearnLine{ sku: "{i.sku}", qty: i.qty }))
}
// Пайплайн отвечает сразу после сохранения фото, обработка продолжается в фоне
rpc uploadPhoto(img) => photoPipeline(callerId(ctx)?, img)
// Вариант поиска выбирает эксперимент «search v2» (src/search, tests/search_ab_test.oxl)
rpc search(q) => searchCatalog(q)
}
// пустая ли корзина (снимок — тип модуля model)
fn isEmpty(s: &CartSnapshot) -> bool => s.items.isEmpty()
policy Shop { allow * if caller is .User(..) }Исходник: examples/shop/src/checkout/checkout.oxl