[{"data":1,"prerenderedAt":8306},["ShallowReactive",2],{"content:\u002F06-architecture\u002F05-clean-practice":3},{"title":4,"description":5,"path":6,"body":7},"Clean Architecture в Go — практика","","\u002F06-architecture\u002F05-clean-practice",{"type":8,"value":9,"toc":8267},"minimark",[10,15,28,32,37,46,50,211,224,228,234,244,248,261,264,283,287,293,300,1385,1559,1562,1568,1810,2485,2491,3169,3479,3485,4224,4231,4238,4921,4931,4935,4939,4942,5012,5015,5042,5054,5058,5067,5120,5123,5145,5149,5155,5161,5290,5309,5313,5316,5519,5522,5526,5529,5593,5981,5984,6179,6182,6193,6297,6304,6308,6312,7039,7046,7050,7285,7288,7292,7296,7351,7354,7358,7547,7551,7654,7658,7770,7774,7868,7871,7875,7907,8103,8263],[11,12,14],"h2",{"id":13},"что-в-этой-теме","Что в этой теме",[16,17,18,19,23,24,27],"p",{},"Полный сквозной пример: от ",[20,21,22],"code",{},"POST \u002Forders"," до ",[20,25,26],{},"INSERT INTO orders",". Структура каталогов, DI руками, ошибки, контекст, логи, метрики, транзакции, тесты, FAQ.",[11,29,31],{"id":30},"структура-и-зависимости","Структура и зависимости",[33,34,36],"h3",{"id":35},"структура-проекта","Структура проекта",[38,39,44],"pre",{"className":40,"code":42,"language":43,"meta":5},[41],"language-text","order-service\u002F\n├── cmd\u002F\n│   └── server\u002F\n│       └── main.go            ← entrypoint, DI, graceful shutdown\n├── internal\u002F\n│   ├── domain\u002F                ← entity, value object, бизнес-правила\n│   │   └── order\u002F\n│   │       ├── order.go\n│   │       ├── status.go\n│   │       └── errors.go\n│   ├── usecase\u002F               ← по файлу на сценарий\n│   │   ├── port.go            ← интерфейсы (порты) repository, gateway\n│   │   ├── create_order.go\n│   │   ├── cancel_order.go\n│   │   └── get_order.go\n│   ├── adapter\u002F\n│   │   ├── http\u002F              ← echo handlers, request\u002Fresponse DTO\n│   │   │   ├── router.go\n│   │   │   ├── order_handler.go\n│   │   │   └── error_mapper.go\n│   │   └── postgres\u002F          ← реализация портов\n│   │       ├── order_repo.go\n│   │       └── tx_manager.go\n│   ├── platform\u002F              ← инфраструктурные адаптеры (logger, metrics, tracer)\n│   │   ├── logger\u002F\n│   │   ├── metrics\u002F\n│   │   └── postgres\u002F          ← пул подключений, миграции\n│   └── config\u002F\n│       └── config.go\n├── pkg\u002F                       ← публично переиспользуемое (если есть)\n├── migrations\u002F\n├── Dockerfile\n└── go.mod\n","text",[20,45,42],{"__ignoreMap":5},[33,47,49],{"id":48},"что-куда-кладётся","Что куда кладётся",[51,52,53,69],"table",{},[54,55,56],"thead",{},[57,58,59,63,66],"tr",{},[60,61,62],"th",{},"Что",[60,64,65],{},"Куда",[60,67,68],{},"Почему",[70,71,72,95,113,129,144,159,177,190],"tbody",{},[57,73,74,87,92],{},[75,76,77,80,81,80,84],"td",{},[20,78,79],{},"Order",", ",[20,82,83],{},"Money",[20,85,86],{},"Status",[75,88,89],{},[20,90,91],{},"internal\u002Fdomain\u002Forder\u002F",[75,93,94],{},"Сущности и инварианты",[57,96,97,105,110],{},[75,98,99,80,102],{},[20,100,101],{},"CreateOrder",[20,103,104],{},"CancelOrder",[75,106,107],{},[20,108,109],{},"internal\u002Fusecase\u002F",[75,111,112],{},"Сценарии приложения",[57,114,115,121,126],{},[75,116,117,120],{},[20,118,119],{},"OrderRepository"," interface",[75,122,123],{},[20,124,125],{},"internal\u002Fusecase\u002Fport.go",[75,127,128],{},"Порт объявляется тем, кто его использует",[57,130,131,136,141],{},[75,132,133],{},[20,134,135],{},"PostgresOrderRepo",[75,137,138],{},[20,139,140],{},"internal\u002Fadapter\u002Fpostgres\u002F",[75,142,143],{},"Адаптер реализует порт",[57,145,146,151,156],{},[75,147,148],{},[20,149,150],{},"OrderHandler",[75,152,153],{},[20,154,155],{},"internal\u002Fadapter\u002Fhttp\u002F",[75,157,158],{},"Транспорт — это адаптер",[57,160,161,169,174],{},[75,162,163,80,166],{},[20,164,165],{},"Logger",[20,167,168],{},"Metrics",[75,170,171],{},[20,172,173],{},"internal\u002Fplatform\u002F",[75,175,176],{},"Кросс-режущие штуки, не бизнес",[57,178,179,182,187],{},[75,180,181],{},"Сборка приложения",[75,183,184],{},[20,185,186],{},"cmd\u002Fserver\u002Fmain.go",[75,188,189],{},"Composition Root",[57,191,192,195,200],{},[75,193,194],{},"Конфиг",[75,196,197],{},[20,198,199],{},"internal\u002Fconfig\u002F",[75,201,202,203,206,207,210],{},"Загрузка ",[20,204,205],{},"env","\u002F",[20,208,209],{},"yaml",", валидация",[16,212,213,216,217,220,221,223],{},[20,214,215],{},"pkg\u002F"," — не используй без необходимости. Любой код с ",[20,218,219],{},"internal\u002F"," приватен для модуля. ",[20,222,215],{}," оправдан только если его реально импортируют сторонние модули.",[33,225,227],{"id":226},"правила-импортов","Правила импортов",[38,229,232],{"className":230,"code":231,"language":43,"meta":5},[41],"domain   ✗→ ничего, кроме stdlib\nusecase  →  domain\nadapter  →  usecase, domain\ncmd      →  всё\nplatform →  только stdlib и сторонние библиотеки\n",[20,233,231],{"__ignoreMap":5},[16,235,236,237,80,240,243],{},"Контролируется ревью + линтером (",[20,238,239],{},"depguard",[20,241,242],{},"go-arch-lint",").",[33,245,247],{"id":246},"как-читать-этот-урок-вместе-с-проектом","Как читать этот урок вместе с проектом",[16,249,250,251,256,257,260],{},"Ниже используется компактный пример с заказом, потому что на нём удобно показать полный вертикальный slice в одном уроке. В отдельном проектном треке та же раскладка применяется к RateDesk на этапе ",[252,253,255],"a",{"href":254},"\u002Fprojects\u002Fratedesk\u002Farchitecture","Architecture",": ",[20,258,259],{},"ConvertAmount",", rate provider boundary, repository\u002Fcache\u002Foutbox ports, idempotency и ADR.",[16,262,263],{},"Важно не копировать имена папок механически. Переносите свойства:",[265,266,267,271,274,277,280],"ul",{},[268,269,270],"li",{},"domain хранит инварианты и value objects;",[268,272,273],{},"use case оркестрирует сценарий и владеет consumer-side ports;",[268,275,276],{},"adapter мапит внешний контракт в модель приложения;",[268,278,279],{},"transaction\u002Fidempotency\u002Foutbox описаны на уровне сценария, а не спрятаны внутри repository;",[268,281,282],{},"observability добавляется на границах, не в entity.",[11,284,286],{"id":285},"сквозной-пример-создание-заказа","Сквозной пример: создание заказа",[16,288,289,290,292],{},"Пройдём ",[20,291,22],{}," сверху вниз — каждый слой со всем кодом.",[33,294,296,297,299],{"id":295},"_1-domain-order-entity","1. Domain — ",[20,298,79],{}," entity",[38,301,305],{"className":302,"code":303,"language":304,"meta":5,"style":5},"language-go shiki shiki-themes github-dark","\u002F\u002F internal\u002Fdomain\u002Forder\u002Forder.go\npackage order\n\nimport (\n    \"errors\"\n    \"time\"\n\n    \"github.com\u002Fgoogle\u002Fuuid\"\n)\n\ntype Status string\n\nconst (\n    StatusDraft     Status = \"draft\"\n    StatusConfirmed Status = \"confirmed\"\n    StatusShipped   Status = \"shipped\"\n    StatusCancelled Status = \"cancelled\"\n)\n\ntype Item struct {\n    SKU      string\n    Quantity int\n    Price    int64 \u002F\u002F в копейках\n}\n\ntype Order struct {\n    id        string\n    userID    string\n    items     []Item\n    status    Status\n    total     int64\n    createdAt time.Time\n}\n\nfunc New(userID string, items []Item, now time.Time) (*Order, error) {\n    if userID == \"\" {\n        return nil, ErrEmptyUser\n    }\n    if len(items) == 0 {\n        return nil, ErrEmptyItems\n    }\n    var total int64\n    for _, it := range items {\n        if it.Quantity \u003C= 0 {\n            return nil, ErrInvalidQuantity\n        }\n        total += it.Price * int64(it.Quantity)\n    }\n    return &Order{\n        id:        uuid.NewString(),\n        userID:    userID,\n        items:     items,\n        status:    StatusDraft,\n        total:     total,\n        createdAt: now,\n    }, nil\n}\n\nfunc (o *Order) ID() string         { return o.id }\nfunc (o *Order) UserID() string     { return o.userID }\nfunc (o *Order) Items() []Item      { return append([]Item(nil), o.items...) }\nfunc (o *Order) Status() Status     { return o.status }\nfunc (o *Order) Total() int64       { return o.total }\nfunc (o *Order) CreatedAt() time.Time { return o.createdAt }\n\nfunc (o *Order) Confirm() error {\n    if o.status != StatusDraft {\n        return ErrInvalidTransition\n    }\n    o.status = StatusConfirmed\n    return nil\n}\n\nfunc (o *Order) Cancel() error {\n    if o.status == StatusShipped {\n        return ErrAlreadyShipped\n    }\n    if o.status == StatusCancelled {\n        return ErrAlreadyCancelled\n    }\n    o.status = StatusCancelled\n    return nil\n}\n\n\u002F\u002F Hydrate — конструктор для репозитория. Только для восстановления из БД.\nfunc Hydrate(id, userID string, items []Item, status Status, total int64, createdAt time.Time) *Order {\n    return &Order{id: id, userID: userID, items: items, status: status, total: total, createdAt: createdAt}\n}\n","go",[20,306,307,316,327,334,344,357,367,372,382,388,393,405,410,418,434,447,461,474,479,484,498,507,516,528,534,539,551,559,567,576,585,594,608,613,618,677,694,706,712,730,740,745,756,774,790,801,807,827,832,846,858,864,870,876,882,888,897,902,907,943,973,1023,1051,1081,1115,1120,1144,1158,1166,1171,1183,1191,1196,1201,1225,1237,1245,1250,1262,1270,1275,1285,1292,1297,1302,1308,1368,1380],{"__ignoreMap":5},[308,309,312],"span",{"class":310,"line":311},"line",1,[308,313,315],{"class":314},"sAwPA","\u002F\u002F internal\u002Fdomain\u002Forder\u002Forder.go\n",[308,317,319,323],{"class":310,"line":318},2,[308,320,322],{"class":321},"snl16","package",[308,324,326],{"class":325},"svObZ"," order\n",[308,328,330],{"class":310,"line":329},3,[308,331,333],{"emptyLinePlaceholder":332},true,"\n",[308,335,337,340],{"class":310,"line":336},4,[308,338,339],{"class":321},"import",[308,341,343],{"class":342},"s95oV"," (\n",[308,345,347,351,354],{"class":310,"line":346},5,[308,348,350],{"class":349},"sU2Wk","    \"",[308,352,353],{"class":325},"errors",[308,355,356],{"class":349},"\"\n",[308,358,360,362,365],{"class":310,"line":359},6,[308,361,350],{"class":349},[308,363,364],{"class":325},"time",[308,366,356],{"class":349},[308,368,370],{"class":310,"line":369},7,[308,371,333],{"emptyLinePlaceholder":332},[308,373,375,377,380],{"class":310,"line":374},8,[308,376,350],{"class":349},[308,378,379],{"class":325},"github.com\u002Fgoogle\u002Fuuid",[308,381,356],{"class":349},[308,383,385],{"class":310,"line":384},9,[308,386,387],{"class":342},")\n",[308,389,391],{"class":310,"line":390},10,[308,392,333],{"emptyLinePlaceholder":332},[308,394,396,399,402],{"class":310,"line":395},11,[308,397,398],{"class":321},"type",[308,400,401],{"class":325}," Status",[308,403,404],{"class":321}," string\n",[308,406,408],{"class":310,"line":407},12,[308,409,333],{"emptyLinePlaceholder":332},[308,411,413,416],{"class":310,"line":412},13,[308,414,415],{"class":321},"const",[308,417,343],{"class":342},[308,419,421,425,428,431],{"class":310,"line":420},14,[308,422,424],{"class":423},"sDLfK","    StatusDraft",[308,426,427],{"class":325},"     Status",[308,429,430],{"class":321}," =",[308,432,433],{"class":349}," \"draft\"\n",[308,435,437,440,442,444],{"class":310,"line":436},15,[308,438,439],{"class":423},"    StatusConfirmed",[308,441,401],{"class":325},[308,443,430],{"class":321},[308,445,446],{"class":349}," \"confirmed\"\n",[308,448,450,453,456,458],{"class":310,"line":449},16,[308,451,452],{"class":423},"    StatusShipped",[308,454,455],{"class":325},"   Status",[308,457,430],{"class":321},[308,459,460],{"class":349}," \"shipped\"\n",[308,462,464,467,469,471],{"class":310,"line":463},17,[308,465,466],{"class":423},"    StatusCancelled",[308,468,401],{"class":325},[308,470,430],{"class":321},[308,472,473],{"class":349}," \"cancelled\"\n",[308,475,477],{"class":310,"line":476},18,[308,478,387],{"class":342},[308,480,482],{"class":310,"line":481},19,[308,483,333],{"emptyLinePlaceholder":332},[308,485,487,489,492,495],{"class":310,"line":486},20,[308,488,398],{"class":321},[308,490,491],{"class":325}," Item",[308,493,494],{"class":321}," struct",[308,496,497],{"class":342}," {\n",[308,499,501,504],{"class":310,"line":500},21,[308,502,503],{"class":342},"    SKU      ",[308,505,506],{"class":321},"string\n",[308,508,510,513],{"class":310,"line":509},22,[308,511,512],{"class":342},"    Quantity ",[308,514,515],{"class":321},"int\n",[308,517,519,522,525],{"class":310,"line":518},23,[308,520,521],{"class":342},"    Price    ",[308,523,524],{"class":321},"int64",[308,526,527],{"class":314}," \u002F\u002F в копейках\n",[308,529,531],{"class":310,"line":530},24,[308,532,533],{"class":342},"}\n",[308,535,537],{"class":310,"line":536},25,[308,538,333],{"emptyLinePlaceholder":332},[308,540,542,544,547,549],{"class":310,"line":541},26,[308,543,398],{"class":321},[308,545,546],{"class":325}," Order",[308,548,494],{"class":321},[308,550,497],{"class":342},[308,552,554,557],{"class":310,"line":553},27,[308,555,556],{"class":342},"    id        ",[308,558,506],{"class":321},[308,560,562,565],{"class":310,"line":561},28,[308,563,564],{"class":342},"    userID    ",[308,566,506],{"class":321},[308,568,570,573],{"class":310,"line":569},29,[308,571,572],{"class":342},"    items     []",[308,574,575],{"class":325},"Item\n",[308,577,579,582],{"class":310,"line":578},30,[308,580,581],{"class":342},"    status    ",[308,583,584],{"class":325},"Status\n",[308,586,588,591],{"class":310,"line":587},31,[308,589,590],{"class":342},"    total     ",[308,592,593],{"class":321},"int64\n",[308,595,597,600,602,605],{"class":310,"line":596},32,[308,598,599],{"class":342},"    createdAt ",[308,601,364],{"class":325},[308,603,604],{"class":342},".",[308,606,607],{"class":325},"Time\n",[308,609,611],{"class":310,"line":610},33,[308,612,533],{"class":342},[308,614,616],{"class":310,"line":615},34,[308,617,333],{"emptyLinePlaceholder":332},[308,619,621,624,627,630,634,637,639,642,645,648,650,653,656,658,661,664,667,669,671,674],{"class":310,"line":620},35,[308,622,623],{"class":321},"func",[308,625,626],{"class":325}," New",[308,628,629],{"class":342},"(",[308,631,633],{"class":632},"s9osk","userID",[308,635,636],{"class":321}," string",[308,638,80],{"class":342},[308,640,641],{"class":632},"items",[308,643,644],{"class":342}," []",[308,646,647],{"class":325},"Item",[308,649,80],{"class":342},[308,651,652],{"class":632},"now",[308,654,655],{"class":325}," time",[308,657,604],{"class":342},[308,659,660],{"class":325},"Time",[308,662,663],{"class":342},") (",[308,665,666],{"class":321},"*",[308,668,79],{"class":325},[308,670,80],{"class":342},[308,672,673],{"class":321},"error",[308,675,676],{"class":342},") {\n",[308,678,680,683,686,689,692],{"class":310,"line":679},36,[308,681,682],{"class":321},"    if",[308,684,685],{"class":342}," userID ",[308,687,688],{"class":321},"==",[308,690,691],{"class":349}," \"\"",[308,693,497],{"class":342},[308,695,697,700,703],{"class":310,"line":696},37,[308,698,699],{"class":321},"        return",[308,701,702],{"class":423}," nil",[308,704,705],{"class":342},", ErrEmptyUser\n",[308,707,709],{"class":310,"line":708},38,[308,710,711],{"class":342},"    }\n",[308,713,715,717,720,723,725,728],{"class":310,"line":714},39,[308,716,682],{"class":321},[308,718,719],{"class":325}," len",[308,721,722],{"class":342},"(items) ",[308,724,688],{"class":321},[308,726,727],{"class":423}," 0",[308,729,497],{"class":342},[308,731,733,735,737],{"class":310,"line":732},40,[308,734,699],{"class":321},[308,736,702],{"class":423},[308,738,739],{"class":342},", ErrEmptyItems\n",[308,741,743],{"class":310,"line":742},41,[308,744,711],{"class":342},[308,746,748,751,754],{"class":310,"line":747},42,[308,749,750],{"class":321},"    var",[308,752,753],{"class":342}," total ",[308,755,593],{"class":321},[308,757,759,762,765,768,771],{"class":310,"line":758},43,[308,760,761],{"class":321},"    for",[308,763,764],{"class":342}," _, it ",[308,766,767],{"class":321},":=",[308,769,770],{"class":321}," range",[308,772,773],{"class":342}," items {\n",[308,775,777,780,783,786,788],{"class":310,"line":776},44,[308,778,779],{"class":321},"        if",[308,781,782],{"class":342}," it.Quantity ",[308,784,785],{"class":321},"\u003C=",[308,787,727],{"class":423},[308,789,497],{"class":342},[308,791,793,796,798],{"class":310,"line":792},45,[308,794,795],{"class":321},"            return",[308,797,702],{"class":423},[308,799,800],{"class":342},", ErrInvalidQuantity\n",[308,802,804],{"class":310,"line":803},46,[308,805,806],{"class":342},"        }\n",[308,808,810,813,816,819,821,824],{"class":310,"line":809},47,[308,811,812],{"class":342},"        total ",[308,814,815],{"class":321},"+=",[308,817,818],{"class":342}," it.Price ",[308,820,666],{"class":321},[308,822,823],{"class":321}," int64",[308,825,826],{"class":342},"(it.Quantity)\n",[308,828,830],{"class":310,"line":829},48,[308,831,711],{"class":342},[308,833,835,838,841,843],{"class":310,"line":834},49,[308,836,837],{"class":321},"    return",[308,839,840],{"class":321}," &",[308,842,79],{"class":325},[308,844,845],{"class":342},"{\n",[308,847,849,852,855],{"class":310,"line":848},50,[308,850,851],{"class":342},"        id:        uuid.",[308,853,854],{"class":325},"NewString",[308,856,857],{"class":342},"(),\n",[308,859,861],{"class":310,"line":860},51,[308,862,863],{"class":342},"        userID:    userID,\n",[308,865,867],{"class":310,"line":866},52,[308,868,869],{"class":342},"        items:     items,\n",[308,871,873],{"class":310,"line":872},53,[308,874,875],{"class":342},"        status:    StatusDraft,\n",[308,877,879],{"class":310,"line":878},54,[308,880,881],{"class":342},"        total:     total,\n",[308,883,885],{"class":310,"line":884},55,[308,886,887],{"class":342},"        createdAt: now,\n",[308,889,891,894],{"class":310,"line":890},56,[308,892,893],{"class":342},"    }, ",[308,895,896],{"class":423},"nil\n",[308,898,900],{"class":310,"line":899},57,[308,901,533],{"class":342},[308,903,905],{"class":310,"line":904},58,[308,906,333],{"emptyLinePlaceholder":332},[308,908,910,912,915,918,920,922,925,928,931,934,937,940],{"class":310,"line":909},59,[308,911,623],{"class":321},[308,913,914],{"class":342}," (",[308,916,917],{"class":632},"o ",[308,919,666],{"class":321},[308,921,79],{"class":325},[308,923,924],{"class":342},") ",[308,926,927],{"class":325},"ID",[308,929,930],{"class":342},"() ",[308,932,933],{"class":321},"string",[308,935,936],{"class":342},"         { ",[308,938,939],{"class":321},"return",[308,941,942],{"class":342}," o.id }\n",[308,944,946,948,950,952,954,956,958,961,963,965,968,970],{"class":310,"line":945},60,[308,947,623],{"class":321},[308,949,914],{"class":342},[308,951,917],{"class":632},[308,953,666],{"class":321},[308,955,79],{"class":325},[308,957,924],{"class":342},[308,959,960],{"class":325},"UserID",[308,962,930],{"class":342},[308,964,933],{"class":321},[308,966,967],{"class":342},"     { ",[308,969,939],{"class":321},[308,971,972],{"class":342}," o.userID }\n",[308,974,976,978,980,982,984,986,988,991,994,996,999,1001,1004,1007,1009,1011,1014,1017,1020],{"class":310,"line":975},61,[308,977,623],{"class":321},[308,979,914],{"class":342},[308,981,917],{"class":632},[308,983,666],{"class":321},[308,985,79],{"class":325},[308,987,924],{"class":342},[308,989,990],{"class":325},"Items",[308,992,993],{"class":342},"() []",[308,995,647],{"class":325},[308,997,998],{"class":342},"      { ",[308,1000,939],{"class":321},[308,1002,1003],{"class":325}," append",[308,1005,1006],{"class":342},"([]",[308,1008,647],{"class":325},[308,1010,629],{"class":342},[308,1012,1013],{"class":423},"nil",[308,1015,1016],{"class":342},"), o.items",[308,1018,1019],{"class":321},"...",[308,1021,1022],{"class":342},") }\n",[308,1024,1026,1028,1030,1032,1034,1036,1038,1040,1042,1044,1046,1048],{"class":310,"line":1025},62,[308,1027,623],{"class":321},[308,1029,914],{"class":342},[308,1031,917],{"class":632},[308,1033,666],{"class":321},[308,1035,79],{"class":325},[308,1037,924],{"class":342},[308,1039,86],{"class":325},[308,1041,930],{"class":342},[308,1043,86],{"class":325},[308,1045,967],{"class":342},[308,1047,939],{"class":321},[308,1049,1050],{"class":342}," o.status }\n",[308,1052,1054,1056,1058,1060,1062,1064,1066,1069,1071,1073,1076,1078],{"class":310,"line":1053},63,[308,1055,623],{"class":321},[308,1057,914],{"class":342},[308,1059,917],{"class":632},[308,1061,666],{"class":321},[308,1063,79],{"class":325},[308,1065,924],{"class":342},[308,1067,1068],{"class":325},"Total",[308,1070,930],{"class":342},[308,1072,524],{"class":321},[308,1074,1075],{"class":342},"       { ",[308,1077,939],{"class":321},[308,1079,1080],{"class":342}," o.total }\n",[308,1082,1084,1086,1088,1090,1092,1094,1096,1099,1101,1103,1105,1107,1110,1112],{"class":310,"line":1083},64,[308,1085,623],{"class":321},[308,1087,914],{"class":342},[308,1089,917],{"class":632},[308,1091,666],{"class":321},[308,1093,79],{"class":325},[308,1095,924],{"class":342},[308,1097,1098],{"class":325},"CreatedAt",[308,1100,930],{"class":342},[308,1102,364],{"class":325},[308,1104,604],{"class":342},[308,1106,660],{"class":325},[308,1108,1109],{"class":342}," { ",[308,1111,939],{"class":321},[308,1113,1114],{"class":342}," o.createdAt }\n",[308,1116,1118],{"class":310,"line":1117},65,[308,1119,333],{"emptyLinePlaceholder":332},[308,1121,1123,1125,1127,1129,1131,1133,1135,1138,1140,1142],{"class":310,"line":1122},66,[308,1124,623],{"class":321},[308,1126,914],{"class":342},[308,1128,917],{"class":632},[308,1130,666],{"class":321},[308,1132,79],{"class":325},[308,1134,924],{"class":342},[308,1136,1137],{"class":325},"Confirm",[308,1139,930],{"class":342},[308,1141,673],{"class":321},[308,1143,497],{"class":342},[308,1145,1147,1149,1152,1155],{"class":310,"line":1146},67,[308,1148,682],{"class":321},[308,1150,1151],{"class":342}," o.status ",[308,1153,1154],{"class":321},"!=",[308,1156,1157],{"class":342}," StatusDraft {\n",[308,1159,1161,1163],{"class":310,"line":1160},68,[308,1162,699],{"class":321},[308,1164,1165],{"class":342}," ErrInvalidTransition\n",[308,1167,1169],{"class":310,"line":1168},69,[308,1170,711],{"class":342},[308,1172,1174,1177,1180],{"class":310,"line":1173},70,[308,1175,1176],{"class":342},"    o.status ",[308,1178,1179],{"class":321},"=",[308,1181,1182],{"class":342}," StatusConfirmed\n",[308,1184,1186,1188],{"class":310,"line":1185},71,[308,1187,837],{"class":321},[308,1189,1190],{"class":423}," nil\n",[308,1192,1194],{"class":310,"line":1193},72,[308,1195,533],{"class":342},[308,1197,1199],{"class":310,"line":1198},73,[308,1200,333],{"emptyLinePlaceholder":332},[308,1202,1204,1206,1208,1210,1212,1214,1216,1219,1221,1223],{"class":310,"line":1203},74,[308,1205,623],{"class":321},[308,1207,914],{"class":342},[308,1209,917],{"class":632},[308,1211,666],{"class":321},[308,1213,79],{"class":325},[308,1215,924],{"class":342},[308,1217,1218],{"class":325},"Cancel",[308,1220,930],{"class":342},[308,1222,673],{"class":321},[308,1224,497],{"class":342},[308,1226,1228,1230,1232,1234],{"class":310,"line":1227},75,[308,1229,682],{"class":321},[308,1231,1151],{"class":342},[308,1233,688],{"class":321},[308,1235,1236],{"class":342}," StatusShipped {\n",[308,1238,1240,1242],{"class":310,"line":1239},76,[308,1241,699],{"class":321},[308,1243,1244],{"class":342}," ErrAlreadyShipped\n",[308,1246,1248],{"class":310,"line":1247},77,[308,1249,711],{"class":342},[308,1251,1253,1255,1257,1259],{"class":310,"line":1252},78,[308,1254,682],{"class":321},[308,1256,1151],{"class":342},[308,1258,688],{"class":321},[308,1260,1261],{"class":342}," StatusCancelled {\n",[308,1263,1265,1267],{"class":310,"line":1264},79,[308,1266,699],{"class":321},[308,1268,1269],{"class":342}," ErrAlreadyCancelled\n",[308,1271,1273],{"class":310,"line":1272},80,[308,1274,711],{"class":342},[308,1276,1278,1280,1282],{"class":310,"line":1277},81,[308,1279,1176],{"class":342},[308,1281,1179],{"class":321},[308,1283,1284],{"class":342}," StatusCancelled\n",[308,1286,1288,1290],{"class":310,"line":1287},82,[308,1289,837],{"class":321},[308,1291,1190],{"class":423},[308,1293,1295],{"class":310,"line":1294},83,[308,1296,533],{"class":342},[308,1298,1300],{"class":310,"line":1299},84,[308,1301,333],{"emptyLinePlaceholder":332},[308,1303,1305],{"class":310,"line":1304},85,[308,1306,1307],{"class":314},"\u002F\u002F Hydrate — конструктор для репозитория. Только для восстановления из БД.\n",[308,1309,1311,1313,1316,1318,1321,1323,1325,1327,1329,1331,1333,1335,1337,1340,1342,1344,1347,1349,1351,1354,1356,1358,1360,1362,1364,1366],{"class":310,"line":1310},86,[308,1312,623],{"class":321},[308,1314,1315],{"class":325}," Hydrate",[308,1317,629],{"class":342},[308,1319,1320],{"class":632},"id",[308,1322,80],{"class":342},[308,1324,633],{"class":632},[308,1326,636],{"class":321},[308,1328,80],{"class":342},[308,1330,641],{"class":632},[308,1332,644],{"class":342},[308,1334,647],{"class":325},[308,1336,80],{"class":342},[308,1338,1339],{"class":632},"status",[308,1341,401],{"class":325},[308,1343,80],{"class":342},[308,1345,1346],{"class":632},"total",[308,1348,823],{"class":321},[308,1350,80],{"class":342},[308,1352,1353],{"class":632},"createdAt",[308,1355,655],{"class":325},[308,1357,604],{"class":342},[308,1359,660],{"class":325},[308,1361,924],{"class":342},[308,1363,666],{"class":321},[308,1365,79],{"class":325},[308,1367,497],{"class":342},[308,1369,1371,1373,1375,1377],{"class":310,"line":1370},87,[308,1372,837],{"class":321},[308,1374,840],{"class":321},[308,1376,79],{"class":325},[308,1378,1379],{"class":342},"{id: id, userID: userID, items: items, status: status, total: total, createdAt: createdAt}\n",[308,1381,1383],{"class":310,"line":1382},88,[308,1384,533],{"class":342},[38,1386,1388],{"className":302,"code":1387,"language":304,"meta":5,"style":5},"\u002F\u002F internal\u002Fdomain\u002Forder\u002Ferrors.go\npackage order\n\nimport \"errors\"\n\nvar (\n    ErrEmptyUser         = errors.New(\"order: empty user\")\n    ErrEmptyItems        = errors.New(\"order: empty items\")\n    ErrInvalidQuantity   = errors.New(\"order: invalid quantity\")\n    ErrInvalidTransition = errors.New(\"order: invalid status transition\")\n    ErrAlreadyShipped    = errors.New(\"order: already shipped\")\n    ErrAlreadyCancelled  = errors.New(\"order: already cancelled\")\n    ErrNotFound          = errors.New(\"order: not found\")\n)\n",[20,1389,1390,1395,1401,1405,1416,1420,1427,1447,1465,1483,1501,1519,1537,1555],{"__ignoreMap":5},[308,1391,1392],{"class":310,"line":311},[308,1393,1394],{"class":314},"\u002F\u002F internal\u002Fdomain\u002Forder\u002Ferrors.go\n",[308,1396,1397,1399],{"class":310,"line":318},[308,1398,322],{"class":321},[308,1400,326],{"class":325},[308,1402,1403],{"class":310,"line":329},[308,1404,333],{"emptyLinePlaceholder":332},[308,1406,1407,1409,1412,1414],{"class":310,"line":336},[308,1408,339],{"class":321},[308,1410,1411],{"class":349}," \"",[308,1413,353],{"class":325},[308,1415,356],{"class":349},[308,1417,1418],{"class":310,"line":346},[308,1419,333],{"emptyLinePlaceholder":332},[308,1421,1422,1425],{"class":310,"line":359},[308,1423,1424],{"class":321},"var",[308,1426,343],{"class":342},[308,1428,1429,1432,1434,1437,1440,1442,1445],{"class":310,"line":369},[308,1430,1431],{"class":342},"    ErrEmptyUser         ",[308,1433,1179],{"class":321},[308,1435,1436],{"class":342}," errors.",[308,1438,1439],{"class":325},"New",[308,1441,629],{"class":342},[308,1443,1444],{"class":349},"\"order: empty user\"",[308,1446,387],{"class":342},[308,1448,1449,1452,1454,1456,1458,1460,1463],{"class":310,"line":374},[308,1450,1451],{"class":342},"    ErrEmptyItems        ",[308,1453,1179],{"class":321},[308,1455,1436],{"class":342},[308,1457,1439],{"class":325},[308,1459,629],{"class":342},[308,1461,1462],{"class":349},"\"order: empty items\"",[308,1464,387],{"class":342},[308,1466,1467,1470,1472,1474,1476,1478,1481],{"class":310,"line":384},[308,1468,1469],{"class":342},"    ErrInvalidQuantity   ",[308,1471,1179],{"class":321},[308,1473,1436],{"class":342},[308,1475,1439],{"class":325},[308,1477,629],{"class":342},[308,1479,1480],{"class":349},"\"order: invalid quantity\"",[308,1482,387],{"class":342},[308,1484,1485,1488,1490,1492,1494,1496,1499],{"class":310,"line":390},[308,1486,1487],{"class":342},"    ErrInvalidTransition ",[308,1489,1179],{"class":321},[308,1491,1436],{"class":342},[308,1493,1439],{"class":325},[308,1495,629],{"class":342},[308,1497,1498],{"class":349},"\"order: invalid status transition\"",[308,1500,387],{"class":342},[308,1502,1503,1506,1508,1510,1512,1514,1517],{"class":310,"line":395},[308,1504,1505],{"class":342},"    ErrAlreadyShipped    ",[308,1507,1179],{"class":321},[308,1509,1436],{"class":342},[308,1511,1439],{"class":325},[308,1513,629],{"class":342},[308,1515,1516],{"class":349},"\"order: already shipped\"",[308,1518,387],{"class":342},[308,1520,1521,1524,1526,1528,1530,1532,1535],{"class":310,"line":407},[308,1522,1523],{"class":342},"    ErrAlreadyCancelled  ",[308,1525,1179],{"class":321},[308,1527,1436],{"class":342},[308,1529,1439],{"class":325},[308,1531,629],{"class":342},[308,1533,1534],{"class":349},"\"order: already cancelled\"",[308,1536,387],{"class":342},[308,1538,1539,1542,1544,1546,1548,1550,1553],{"class":310,"line":412},[308,1540,1541],{"class":342},"    ErrNotFound          ",[308,1543,1179],{"class":321},[308,1545,1436],{"class":342},[308,1547,1439],{"class":325},[308,1549,629],{"class":342},[308,1551,1552],{"class":349},"\"order: not found\"",[308,1554,387],{"class":342},[308,1556,1557],{"class":310,"line":420},[308,1558,387],{"class":342},[16,1560,1561],{},"Заметь: поля приватные, инварианты — в конструкторе и методах. Это «толстый» domain, а не анемичный bag-of-data.",[33,1563,1565,1566],{"id":1564},"_2-use-case-createorder","2. Use Case — ",[20,1567,101],{},[38,1569,1571],{"className":302,"code":1570,"language":304,"meta":5,"style":5},"\u002F\u002F internal\u002Fusecase\u002Fport.go\npackage usecase\n\nimport (\n    \"context\"\n    \"time\"\n\n    \"myproject\u002Finternal\u002Fdomain\u002Forder\"\n)\n\ntype OrderRepository interface {\n    Save(ctx context.Context, o *order.Order) error\n    FindByID(ctx context.Context, id string) (*order.Order, error)\n}\n\ntype Clock interface {\n    Now() time.Time\n}\n\ntype EventPublisher interface {\n    Publish(ctx context.Context, topic string, payload []byte) error\n}\n",[20,1572,1573,1578,1585,1589,1595,1604,1612,1616,1625,1629,1633,1644,1682,1719,1723,1727,1738,1751,1755,1759,1770,1806],{"__ignoreMap":5},[308,1574,1575],{"class":310,"line":311},[308,1576,1577],{"class":314},"\u002F\u002F internal\u002Fusecase\u002Fport.go\n",[308,1579,1580,1582],{"class":310,"line":318},[308,1581,322],{"class":321},[308,1583,1584],{"class":325}," usecase\n",[308,1586,1587],{"class":310,"line":329},[308,1588,333],{"emptyLinePlaceholder":332},[308,1590,1591,1593],{"class":310,"line":336},[308,1592,339],{"class":321},[308,1594,343],{"class":342},[308,1596,1597,1599,1602],{"class":310,"line":346},[308,1598,350],{"class":349},[308,1600,1601],{"class":325},"context",[308,1603,356],{"class":349},[308,1605,1606,1608,1610],{"class":310,"line":359},[308,1607,350],{"class":349},[308,1609,364],{"class":325},[308,1611,356],{"class":349},[308,1613,1614],{"class":310,"line":369},[308,1615,333],{"emptyLinePlaceholder":332},[308,1617,1618,1620,1623],{"class":310,"line":374},[308,1619,350],{"class":349},[308,1621,1622],{"class":325},"myproject\u002Finternal\u002Fdomain\u002Forder",[308,1624,356],{"class":349},[308,1626,1627],{"class":310,"line":384},[308,1628,387],{"class":342},[308,1630,1631],{"class":310,"line":390},[308,1632,333],{"emptyLinePlaceholder":332},[308,1634,1635,1637,1640,1642],{"class":310,"line":395},[308,1636,398],{"class":321},[308,1638,1639],{"class":325}," OrderRepository",[308,1641,120],{"class":321},[308,1643,497],{"class":342},[308,1645,1646,1649,1651,1654,1657,1659,1662,1664,1667,1670,1673,1675,1677,1679],{"class":310,"line":407},[308,1647,1648],{"class":325},"    Save",[308,1650,629],{"class":342},[308,1652,1653],{"class":632},"ctx",[308,1655,1656],{"class":325}," context",[308,1658,604],{"class":342},[308,1660,1661],{"class":325},"Context",[308,1663,80],{"class":342},[308,1665,1666],{"class":632},"o",[308,1668,1669],{"class":321}," *",[308,1671,1672],{"class":325},"order",[308,1674,604],{"class":342},[308,1676,79],{"class":325},[308,1678,924],{"class":342},[308,1680,1681],{"class":321},"error\n",[308,1683,1684,1687,1689,1691,1693,1695,1697,1699,1701,1703,1705,1707,1709,1711,1713,1715,1717],{"class":310,"line":412},[308,1685,1686],{"class":325},"    FindByID",[308,1688,629],{"class":342},[308,1690,1653],{"class":632},[308,1692,1656],{"class":325},[308,1694,604],{"class":342},[308,1696,1661],{"class":325},[308,1698,80],{"class":342},[308,1700,1320],{"class":632},[308,1702,636],{"class":321},[308,1704,663],{"class":342},[308,1706,666],{"class":321},[308,1708,1672],{"class":325},[308,1710,604],{"class":342},[308,1712,79],{"class":325},[308,1714,80],{"class":342},[308,1716,673],{"class":321},[308,1718,387],{"class":342},[308,1720,1721],{"class":310,"line":420},[308,1722,533],{"class":342},[308,1724,1725],{"class":310,"line":436},[308,1726,333],{"emptyLinePlaceholder":332},[308,1728,1729,1731,1734,1736],{"class":310,"line":449},[308,1730,398],{"class":321},[308,1732,1733],{"class":325}," Clock",[308,1735,120],{"class":321},[308,1737,497],{"class":342},[308,1739,1740,1743,1745,1747,1749],{"class":310,"line":463},[308,1741,1742],{"class":325},"    Now",[308,1744,930],{"class":342},[308,1746,364],{"class":325},[308,1748,604],{"class":342},[308,1750,607],{"class":325},[308,1752,1753],{"class":310,"line":476},[308,1754,533],{"class":342},[308,1756,1757],{"class":310,"line":481},[308,1758,333],{"emptyLinePlaceholder":332},[308,1760,1761,1763,1766,1768],{"class":310,"line":486},[308,1762,398],{"class":321},[308,1764,1765],{"class":325}," EventPublisher",[308,1767,120],{"class":321},[308,1769,497],{"class":342},[308,1771,1772,1775,1777,1779,1781,1783,1785,1787,1790,1792,1794,1797,1799,1802,1804],{"class":310,"line":500},[308,1773,1774],{"class":325},"    Publish",[308,1776,629],{"class":342},[308,1778,1653],{"class":632},[308,1780,1656],{"class":325},[308,1782,604],{"class":342},[308,1784,1661],{"class":325},[308,1786,80],{"class":342},[308,1788,1789],{"class":632},"topic",[308,1791,636],{"class":321},[308,1793,80],{"class":342},[308,1795,1796],{"class":632},"payload",[308,1798,644],{"class":342},[308,1800,1801],{"class":321},"byte",[308,1803,924],{"class":342},[308,1805,1681],{"class":321},[308,1807,1808],{"class":310,"line":509},[308,1809,533],{"class":342},[38,1811,1813],{"className":302,"code":1812,"language":304,"meta":5,"style":5},"\u002F\u002F internal\u002Fusecase\u002Fcreate_order.go\npackage usecase\n\nimport (\n    \"context\"\n    \"encoding\u002Fjson\"\n    \"fmt\"\n\n    \"myproject\u002Finternal\u002Fdomain\u002Forder\"\n)\n\ntype CreateOrderInput struct {\n    UserID string\n    Items  []CreateOrderItem\n}\n\ntype CreateOrderItem struct {\n    SKU      string\n    Quantity int\n    Price    int64\n}\n\ntype CreateOrderOutput struct {\n    OrderID string\n    Total   int64\n}\n\ntype CreateOrder struct {\n    repo      OrderRepository\n    publisher EventPublisher\n    clock     Clock\n}\n\nfunc NewCreateOrder(repo OrderRepository, publisher EventPublisher, clock Clock) *CreateOrder {\n    return &CreateOrder{repo: repo, publisher: publisher, clock: clock}\n}\n\nfunc (uc *CreateOrder) Execute(ctx context.Context, in CreateOrderInput) (CreateOrderOutput, error) {\n    items := make([]order.Item, 0, len(in.Items))\n    for _, it := range in.Items {\n        items = append(items, order.Item{SKU: it.SKU, Quantity: it.Quantity, Price: it.Price})\n    }\n\n    o, err := order.New(in.UserID, items, uc.clock.Now())\n    if err != nil {\n        return CreateOrderOutput{}, fmt.Errorf(\"create order: %w\", err)\n    }\n\n    if err := uc.repo.Save(ctx, o); err != nil {\n        return CreateOrderOutput{}, fmt.Errorf(\"save order: %w\", err)\n    }\n\n    payload, err := json.Marshal(map[string]any{\"order_id\": o.ID(), \"user_id\": o.UserID()})\n    if err != nil {\n        return CreateOrderOutput{}, fmt.Errorf(\"marshal order created event: %w\", err)\n    }\n    if err := uc.publisher.Publish(ctx, \"orders.created\", payload); err != nil {\n        \u002F\u002F событие — не блокирующий шаг, логируем но не валим транзакцию\n        \u002F\u002F в реальности — outbox pattern, см. ниже\n    }\n\n    return CreateOrderOutput{OrderID: o.ID(), Total: o.Total()}, nil\n}\n",[20,1814,1815,1820,1826,1830,1836,1844,1853,1862,1866,1874,1878,1882,1893,1900,1908,1912,1916,1927,1933,1939,1945,1949,1953,1964,1971,1978,1982,1986,1997,2005,2013,2021,2025,2029,2065,2076,2080,2084,2130,2161,2174,2195,2199,2203,2224,2237,2263,2267,2271,2294,2315,2319,2323,2376,2388,2409,2413,2442,2447,2452,2456,2460,2481],{"__ignoreMap":5},[308,1816,1817],{"class":310,"line":311},[308,1818,1819],{"class":314},"\u002F\u002F internal\u002Fusecase\u002Fcreate_order.go\n",[308,1821,1822,1824],{"class":310,"line":318},[308,1823,322],{"class":321},[308,1825,1584],{"class":325},[308,1827,1828],{"class":310,"line":329},[308,1829,333],{"emptyLinePlaceholder":332},[308,1831,1832,1834],{"class":310,"line":336},[308,1833,339],{"class":321},[308,1835,343],{"class":342},[308,1837,1838,1840,1842],{"class":310,"line":346},[308,1839,350],{"class":349},[308,1841,1601],{"class":325},[308,1843,356],{"class":349},[308,1845,1846,1848,1851],{"class":310,"line":359},[308,1847,350],{"class":349},[308,1849,1850],{"class":325},"encoding\u002Fjson",[308,1852,356],{"class":349},[308,1854,1855,1857,1860],{"class":310,"line":369},[308,1856,350],{"class":349},[308,1858,1859],{"class":325},"fmt",[308,1861,356],{"class":349},[308,1863,1864],{"class":310,"line":374},[308,1865,333],{"emptyLinePlaceholder":332},[308,1867,1868,1870,1872],{"class":310,"line":384},[308,1869,350],{"class":349},[308,1871,1622],{"class":325},[308,1873,356],{"class":349},[308,1875,1876],{"class":310,"line":390},[308,1877,387],{"class":342},[308,1879,1880],{"class":310,"line":395},[308,1881,333],{"emptyLinePlaceholder":332},[308,1883,1884,1886,1889,1891],{"class":310,"line":407},[308,1885,398],{"class":321},[308,1887,1888],{"class":325}," CreateOrderInput",[308,1890,494],{"class":321},[308,1892,497],{"class":342},[308,1894,1895,1898],{"class":310,"line":412},[308,1896,1897],{"class":342},"    UserID ",[308,1899,506],{"class":321},[308,1901,1902,1905],{"class":310,"line":420},[308,1903,1904],{"class":342},"    Items  []",[308,1906,1907],{"class":325},"CreateOrderItem\n",[308,1909,1910],{"class":310,"line":436},[308,1911,533],{"class":342},[308,1913,1914],{"class":310,"line":449},[308,1915,333],{"emptyLinePlaceholder":332},[308,1917,1918,1920,1923,1925],{"class":310,"line":463},[308,1919,398],{"class":321},[308,1921,1922],{"class":325}," CreateOrderItem",[308,1924,494],{"class":321},[308,1926,497],{"class":342},[308,1928,1929,1931],{"class":310,"line":476},[308,1930,503],{"class":342},[308,1932,506],{"class":321},[308,1934,1935,1937],{"class":310,"line":481},[308,1936,512],{"class":342},[308,1938,515],{"class":321},[308,1940,1941,1943],{"class":310,"line":486},[308,1942,521],{"class":342},[308,1944,593],{"class":321},[308,1946,1947],{"class":310,"line":500},[308,1948,533],{"class":342},[308,1950,1951],{"class":310,"line":509},[308,1952,333],{"emptyLinePlaceholder":332},[308,1954,1955,1957,1960,1962],{"class":310,"line":518},[308,1956,398],{"class":321},[308,1958,1959],{"class":325}," CreateOrderOutput",[308,1961,494],{"class":321},[308,1963,497],{"class":342},[308,1965,1966,1969],{"class":310,"line":530},[308,1967,1968],{"class":342},"    OrderID ",[308,1970,506],{"class":321},[308,1972,1973,1976],{"class":310,"line":536},[308,1974,1975],{"class":342},"    Total   ",[308,1977,593],{"class":321},[308,1979,1980],{"class":310,"line":541},[308,1981,533],{"class":342},[308,1983,1984],{"class":310,"line":553},[308,1985,333],{"emptyLinePlaceholder":332},[308,1987,1988,1990,1993,1995],{"class":310,"line":561},[308,1989,398],{"class":321},[308,1991,1992],{"class":325}," CreateOrder",[308,1994,494],{"class":321},[308,1996,497],{"class":342},[308,1998,1999,2002],{"class":310,"line":569},[308,2000,2001],{"class":342},"    repo      ",[308,2003,2004],{"class":325},"OrderRepository\n",[308,2006,2007,2010],{"class":310,"line":578},[308,2008,2009],{"class":342},"    publisher ",[308,2011,2012],{"class":325},"EventPublisher\n",[308,2014,2015,2018],{"class":310,"line":587},[308,2016,2017],{"class":342},"    clock     ",[308,2019,2020],{"class":325},"Clock\n",[308,2022,2023],{"class":310,"line":596},[308,2024,533],{"class":342},[308,2026,2027],{"class":310,"line":610},[308,2028,333],{"emptyLinePlaceholder":332},[308,2030,2031,2033,2036,2038,2041,2043,2045,2048,2050,2052,2055,2057,2059,2061,2063],{"class":310,"line":615},[308,2032,623],{"class":321},[308,2034,2035],{"class":325}," NewCreateOrder",[308,2037,629],{"class":342},[308,2039,2040],{"class":632},"repo",[308,2042,1639],{"class":325},[308,2044,80],{"class":342},[308,2046,2047],{"class":632},"publisher",[308,2049,1765],{"class":325},[308,2051,80],{"class":342},[308,2053,2054],{"class":632},"clock",[308,2056,1733],{"class":325},[308,2058,924],{"class":342},[308,2060,666],{"class":321},[308,2062,101],{"class":325},[308,2064,497],{"class":342},[308,2066,2067,2069,2071,2073],{"class":310,"line":620},[308,2068,837],{"class":321},[308,2070,840],{"class":321},[308,2072,101],{"class":325},[308,2074,2075],{"class":342},"{repo: repo, publisher: publisher, clock: clock}\n",[308,2077,2078],{"class":310,"line":679},[308,2079,533],{"class":342},[308,2081,2082],{"class":310,"line":696},[308,2083,333],{"emptyLinePlaceholder":332},[308,2085,2086,2088,2090,2093,2095,2097,2099,2102,2104,2106,2108,2110,2112,2114,2117,2119,2121,2124,2126,2128],{"class":310,"line":708},[308,2087,623],{"class":321},[308,2089,914],{"class":342},[308,2091,2092],{"class":632},"uc ",[308,2094,666],{"class":321},[308,2096,101],{"class":325},[308,2098,924],{"class":342},[308,2100,2101],{"class":325},"Execute",[308,2103,629],{"class":342},[308,2105,1653],{"class":632},[308,2107,1656],{"class":325},[308,2109,604],{"class":342},[308,2111,1661],{"class":325},[308,2113,80],{"class":342},[308,2115,2116],{"class":632},"in",[308,2118,1888],{"class":325},[308,2120,663],{"class":342},[308,2122,2123],{"class":325},"CreateOrderOutput",[308,2125,80],{"class":342},[308,2127,673],{"class":321},[308,2129,676],{"class":342},[308,2131,2132,2135,2137,2140,2142,2144,2146,2148,2150,2153,2155,2158],{"class":310,"line":714},[308,2133,2134],{"class":342},"    items ",[308,2136,767],{"class":321},[308,2138,2139],{"class":325}," make",[308,2141,1006],{"class":342},[308,2143,1672],{"class":325},[308,2145,604],{"class":342},[308,2147,647],{"class":325},[308,2149,80],{"class":342},[308,2151,2152],{"class":423},"0",[308,2154,80],{"class":342},[308,2156,2157],{"class":325},"len",[308,2159,2160],{"class":342},"(in.Items))\n",[308,2162,2163,2165,2167,2169,2171],{"class":310,"line":732},[308,2164,761],{"class":321},[308,2166,764],{"class":342},[308,2168,767],{"class":321},[308,2170,770],{"class":321},[308,2172,2173],{"class":342}," in.Items {\n",[308,2175,2176,2179,2181,2183,2186,2188,2190,2192],{"class":310,"line":742},[308,2177,2178],{"class":342},"        items ",[308,2180,1179],{"class":321},[308,2182,1003],{"class":325},[308,2184,2185],{"class":342},"(items, ",[308,2187,1672],{"class":325},[308,2189,604],{"class":342},[308,2191,647],{"class":325},[308,2193,2194],{"class":342},"{SKU: it.SKU, Quantity: it.Quantity, Price: it.Price})\n",[308,2196,2197],{"class":310,"line":747},[308,2198,711],{"class":342},[308,2200,2201],{"class":310,"line":758},[308,2202,333],{"emptyLinePlaceholder":332},[308,2204,2205,2208,2210,2213,2215,2218,2221],{"class":310,"line":776},[308,2206,2207],{"class":342},"    o, err ",[308,2209,767],{"class":321},[308,2211,2212],{"class":342}," order.",[308,2214,1439],{"class":325},[308,2216,2217],{"class":342},"(in.UserID, items, uc.clock.",[308,2219,2220],{"class":325},"Now",[308,2222,2223],{"class":342},"())\n",[308,2225,2226,2228,2231,2233,2235],{"class":310,"line":792},[308,2227,682],{"class":321},[308,2229,2230],{"class":342}," err ",[308,2232,1154],{"class":321},[308,2234,702],{"class":423},[308,2236,497],{"class":342},[308,2238,2239,2241,2243,2246,2249,2251,2254,2257,2260],{"class":310,"line":803},[308,2240,699],{"class":321},[308,2242,1959],{"class":325},[308,2244,2245],{"class":342},"{}, fmt.",[308,2247,2248],{"class":325},"Errorf",[308,2250,629],{"class":342},[308,2252,2253],{"class":349},"\"create order: ",[308,2255,2256],{"class":423},"%w",[308,2258,2259],{"class":349},"\"",[308,2261,2262],{"class":342},", err)\n",[308,2264,2265],{"class":310,"line":809},[308,2266,711],{"class":342},[308,2268,2269],{"class":310,"line":829},[308,2270,333],{"emptyLinePlaceholder":332},[308,2272,2273,2275,2277,2279,2282,2285,2288,2290,2292],{"class":310,"line":834},[308,2274,682],{"class":321},[308,2276,2230],{"class":342},[308,2278,767],{"class":321},[308,2280,2281],{"class":342}," uc.repo.",[308,2283,2284],{"class":325},"Save",[308,2286,2287],{"class":342},"(ctx, o); err ",[308,2289,1154],{"class":321},[308,2291,702],{"class":423},[308,2293,497],{"class":342},[308,2295,2296,2298,2300,2302,2304,2306,2309,2311,2313],{"class":310,"line":848},[308,2297,699],{"class":321},[308,2299,1959],{"class":325},[308,2301,2245],{"class":342},[308,2303,2248],{"class":325},[308,2305,629],{"class":342},[308,2307,2308],{"class":349},"\"save order: ",[308,2310,2256],{"class":423},[308,2312,2259],{"class":349},[308,2314,2262],{"class":342},[308,2316,2317],{"class":310,"line":860},[308,2318,711],{"class":342},[308,2320,2321],{"class":310,"line":866},[308,2322,333],{"emptyLinePlaceholder":332},[308,2324,2325,2328,2330,2333,2336,2338,2341,2344,2346,2349,2352,2355,2358,2361,2363,2366,2369,2371,2373],{"class":310,"line":872},[308,2326,2327],{"class":342},"    payload, err ",[308,2329,767],{"class":321},[308,2331,2332],{"class":342}," json.",[308,2334,2335],{"class":325},"Marshal",[308,2337,629],{"class":342},[308,2339,2340],{"class":321},"map",[308,2342,2343],{"class":342},"[",[308,2345,933],{"class":321},[308,2347,2348],{"class":342},"]",[308,2350,2351],{"class":325},"any",[308,2353,2354],{"class":342},"{",[308,2356,2357],{"class":349},"\"order_id\"",[308,2359,2360],{"class":342},": o.",[308,2362,927],{"class":325},[308,2364,2365],{"class":342},"(), ",[308,2367,2368],{"class":349},"\"user_id\"",[308,2370,2360],{"class":342},[308,2372,960],{"class":325},[308,2374,2375],{"class":342},"()})\n",[308,2377,2378,2380,2382,2384,2386],{"class":310,"line":878},[308,2379,682],{"class":321},[308,2381,2230],{"class":342},[308,2383,1154],{"class":321},[308,2385,702],{"class":423},[308,2387,497],{"class":342},[308,2389,2390,2392,2394,2396,2398,2400,2403,2405,2407],{"class":310,"line":884},[308,2391,699],{"class":321},[308,2393,1959],{"class":325},[308,2395,2245],{"class":342},[308,2397,2248],{"class":325},[308,2399,629],{"class":342},[308,2401,2402],{"class":349},"\"marshal order created event: ",[308,2404,2256],{"class":423},[308,2406,2259],{"class":349},[308,2408,2262],{"class":342},[308,2410,2411],{"class":310,"line":890},[308,2412,711],{"class":342},[308,2414,2415,2417,2419,2421,2424,2427,2430,2433,2436,2438,2440],{"class":310,"line":899},[308,2416,682],{"class":321},[308,2418,2230],{"class":342},[308,2420,767],{"class":321},[308,2422,2423],{"class":342}," uc.publisher.",[308,2425,2426],{"class":325},"Publish",[308,2428,2429],{"class":342},"(ctx, ",[308,2431,2432],{"class":349},"\"orders.created\"",[308,2434,2435],{"class":342},", payload); err ",[308,2437,1154],{"class":321},[308,2439,702],{"class":423},[308,2441,497],{"class":342},[308,2443,2444],{"class":310,"line":904},[308,2445,2446],{"class":314},"        \u002F\u002F событие — не блокирующий шаг, логируем но не валим транзакцию\n",[308,2448,2449],{"class":310,"line":909},[308,2450,2451],{"class":314},"        \u002F\u002F в реальности — outbox pattern, см. ниже\n",[308,2453,2454],{"class":310,"line":945},[308,2455,711],{"class":342},[308,2457,2458],{"class":310,"line":975},[308,2459,333],{"emptyLinePlaceholder":332},[308,2461,2462,2464,2466,2469,2471,2474,2476,2479],{"class":310,"line":1025},[308,2463,837],{"class":321},[308,2465,1959],{"class":325},[308,2467,2468],{"class":342},"{OrderID: o.",[308,2470,927],{"class":325},[308,2472,2473],{"class":342},"(), Total: o.",[308,2475,1068],{"class":325},[308,2477,2478],{"class":342},"()}, ",[308,2480,896],{"class":423},[308,2482,2483],{"class":310,"line":1053},[308,2484,533],{"class":342},[33,2486,2488,2489],{"id":2487},"_3-http-handler-orderhandler","3. HTTP handler — ",[20,2490,150],{},[38,2492,2494],{"className":302,"code":2493,"language":304,"meta":5,"style":5},"\u002F\u002F internal\u002Fadapter\u002Fhttp\u002Forder_handler.go\npackage httpadapter\n\nimport (\n    \"errors\"\n    \"net\u002Fhttp\"\n\n    \"github.com\u002Flabstack\u002Fecho\u002Fv4\"\n\n    \"myproject\u002Finternal\u002Fdomain\u002Forder\"\n    \"myproject\u002Finternal\u002Fusecase\"\n)\n\ntype OrderHandler struct {\n    create *usecase.CreateOrder\n    cancel *usecase.CancelOrder\n    get    *usecase.GetOrder\n}\n\nfunc NewOrderHandler(c *usecase.CreateOrder, cn *usecase.CancelOrder, g *usecase.GetOrder) *OrderHandler {\n    return &OrderHandler{create: c, cancel: cn, get: g}\n}\n\ntype createOrderRequest struct {\n    UserID string             `json:\"user_id\" validate:\"required\"`\n    Items  []createOrderItem  `json:\"items\" validate:\"required,min=1,dive\"`\n}\n\ntype createOrderItem struct {\n    SKU      string `json:\"sku\" validate:\"required\"`\n    Quantity int    `json:\"quantity\" validate:\"required,min=1\"`\n    Price    int64  `json:\"price\" validate:\"required,min=0\"`\n}\n\ntype createOrderResponse struct {\n    OrderID string `json:\"order_id\"`\n    Total   int64  `json:\"total\"`\n}\n\nfunc (h *OrderHandler) Create(c echo.Context) error {\n    var req createOrderRequest\n    if err := c.Bind(&req); err != nil {\n        return c.JSON(http.StatusBadRequest, errorResp{Error: \"invalid body\"})\n    }\n    if err := c.Validate(&req); err != nil {\n        return c.JSON(http.StatusBadRequest, errorResp{Error: err.Error()})\n    }\n\n    items := make([]usecase.CreateOrderItem, 0, len(req.Items))\n    for _, it := range req.Items {\n        items = append(items, usecase.CreateOrderItem{SKU: it.SKU, Quantity: it.Quantity, Price: it.Price})\n    }\n\n    out, err := h.create.Execute(c.Request().Context(), usecase.CreateOrderInput{\n        UserID: req.UserID,\n        Items:  items,\n    })\n    if err != nil {\n        return mapError(c, err)\n    }\n\n    return c.JSON(http.StatusCreated, createOrderResponse{\n        OrderID: out.OrderID,\n        Total:   out.Total,\n    })\n}\n",[20,2495,2496,2501,2508,2512,2518,2526,2535,2539,2548,2552,2560,2569,2573,2577,2588,2603,2617,2631,2635,2639,2694,2705,2709,2713,2724,2733,2743,2747,2751,2762,2771,2781,2790,2794,2798,2809,2818,2827,2831,2835,2870,2880,2908,2932,2936,2961,2981,2985,2989,3017,3030,3048,3052,3056,3090,3095,3100,3105,3117,3127,3131,3135,3151,3156,3161,3165],{"__ignoreMap":5},[308,2497,2498],{"class":310,"line":311},[308,2499,2500],{"class":314},"\u002F\u002F internal\u002Fadapter\u002Fhttp\u002Forder_handler.go\n",[308,2502,2503,2505],{"class":310,"line":318},[308,2504,322],{"class":321},[308,2506,2507],{"class":325}," httpadapter\n",[308,2509,2510],{"class":310,"line":329},[308,2511,333],{"emptyLinePlaceholder":332},[308,2513,2514,2516],{"class":310,"line":336},[308,2515,339],{"class":321},[308,2517,343],{"class":342},[308,2519,2520,2522,2524],{"class":310,"line":346},[308,2521,350],{"class":349},[308,2523,353],{"class":325},[308,2525,356],{"class":349},[308,2527,2528,2530,2533],{"class":310,"line":359},[308,2529,350],{"class":349},[308,2531,2532],{"class":325},"net\u002Fhttp",[308,2534,356],{"class":349},[308,2536,2537],{"class":310,"line":369},[308,2538,333],{"emptyLinePlaceholder":332},[308,2540,2541,2543,2546],{"class":310,"line":374},[308,2542,350],{"class":349},[308,2544,2545],{"class":325},"github.com\u002Flabstack\u002Fecho\u002Fv4",[308,2547,356],{"class":349},[308,2549,2550],{"class":310,"line":384},[308,2551,333],{"emptyLinePlaceholder":332},[308,2553,2554,2556,2558],{"class":310,"line":390},[308,2555,350],{"class":349},[308,2557,1622],{"class":325},[308,2559,356],{"class":349},[308,2561,2562,2564,2567],{"class":310,"line":395},[308,2563,350],{"class":349},[308,2565,2566],{"class":325},"myproject\u002Finternal\u002Fusecase",[308,2568,356],{"class":349},[308,2570,2571],{"class":310,"line":407},[308,2572,387],{"class":342},[308,2574,2575],{"class":310,"line":412},[308,2576,333],{"emptyLinePlaceholder":332},[308,2578,2579,2581,2584,2586],{"class":310,"line":420},[308,2580,398],{"class":321},[308,2582,2583],{"class":325}," OrderHandler",[308,2585,494],{"class":321},[308,2587,497],{"class":342},[308,2589,2590,2593,2595,2598,2600],{"class":310,"line":436},[308,2591,2592],{"class":342},"    create ",[308,2594,666],{"class":321},[308,2596,2597],{"class":325},"usecase",[308,2599,604],{"class":342},[308,2601,2602],{"class":325},"CreateOrder\n",[308,2604,2605,2608,2610,2612,2614],{"class":310,"line":449},[308,2606,2607],{"class":342},"    cancel ",[308,2609,666],{"class":321},[308,2611,2597],{"class":325},[308,2613,604],{"class":342},[308,2615,2616],{"class":325},"CancelOrder\n",[308,2618,2619,2622,2624,2626,2628],{"class":310,"line":463},[308,2620,2621],{"class":342},"    get    ",[308,2623,666],{"class":321},[308,2625,2597],{"class":325},[308,2627,604],{"class":342},[308,2629,2630],{"class":325},"GetOrder\n",[308,2632,2633],{"class":310,"line":476},[308,2634,533],{"class":342},[308,2636,2637],{"class":310,"line":481},[308,2638,333],{"emptyLinePlaceholder":332},[308,2640,2641,2643,2646,2648,2651,2653,2655,2657,2659,2661,2664,2666,2668,2670,2672,2674,2677,2679,2681,2683,2686,2688,2690,2692],{"class":310,"line":486},[308,2642,623],{"class":321},[308,2644,2645],{"class":325}," NewOrderHandler",[308,2647,629],{"class":342},[308,2649,2650],{"class":632},"c",[308,2652,1669],{"class":321},[308,2654,2597],{"class":325},[308,2656,604],{"class":342},[308,2658,101],{"class":325},[308,2660,80],{"class":342},[308,2662,2663],{"class":632},"cn",[308,2665,1669],{"class":321},[308,2667,2597],{"class":325},[308,2669,604],{"class":342},[308,2671,104],{"class":325},[308,2673,80],{"class":342},[308,2675,2676],{"class":632},"g",[308,2678,1669],{"class":321},[308,2680,2597],{"class":325},[308,2682,604],{"class":342},[308,2684,2685],{"class":325},"GetOrder",[308,2687,924],{"class":342},[308,2689,666],{"class":321},[308,2691,150],{"class":325},[308,2693,497],{"class":342},[308,2695,2696,2698,2700,2702],{"class":310,"line":500},[308,2697,837],{"class":321},[308,2699,840],{"class":321},[308,2701,150],{"class":325},[308,2703,2704],{"class":342},"{create: c, cancel: cn, get: g}\n",[308,2706,2707],{"class":310,"line":509},[308,2708,533],{"class":342},[308,2710,2711],{"class":310,"line":518},[308,2712,333],{"emptyLinePlaceholder":332},[308,2714,2715,2717,2720,2722],{"class":310,"line":530},[308,2716,398],{"class":321},[308,2718,2719],{"class":325}," createOrderRequest",[308,2721,494],{"class":321},[308,2723,497],{"class":342},[308,2725,2726,2728,2730],{"class":310,"line":536},[308,2727,1897],{"class":342},[308,2729,933],{"class":321},[308,2731,2732],{"class":349},"             `json:\"user_id\" validate:\"required\"`\n",[308,2734,2735,2737,2740],{"class":310,"line":541},[308,2736,1904],{"class":342},[308,2738,2739],{"class":325},"createOrderItem",[308,2741,2742],{"class":349},"  `json:\"items\" validate:\"required,min=1,dive\"`\n",[308,2744,2745],{"class":310,"line":553},[308,2746,533],{"class":342},[308,2748,2749],{"class":310,"line":561},[308,2750,333],{"emptyLinePlaceholder":332},[308,2752,2753,2755,2758,2760],{"class":310,"line":569},[308,2754,398],{"class":321},[308,2756,2757],{"class":325}," createOrderItem",[308,2759,494],{"class":321},[308,2761,497],{"class":342},[308,2763,2764,2766,2768],{"class":310,"line":578},[308,2765,503],{"class":342},[308,2767,933],{"class":321},[308,2769,2770],{"class":349}," `json:\"sku\" validate:\"required\"`\n",[308,2772,2773,2775,2778],{"class":310,"line":587},[308,2774,512],{"class":342},[308,2776,2777],{"class":321},"int",[308,2779,2780],{"class":349},"    `json:\"quantity\" validate:\"required,min=1\"`\n",[308,2782,2783,2785,2787],{"class":310,"line":596},[308,2784,521],{"class":342},[308,2786,524],{"class":321},[308,2788,2789],{"class":349},"  `json:\"price\" validate:\"required,min=0\"`\n",[308,2791,2792],{"class":310,"line":610},[308,2793,533],{"class":342},[308,2795,2796],{"class":310,"line":615},[308,2797,333],{"emptyLinePlaceholder":332},[308,2799,2800,2802,2805,2807],{"class":310,"line":620},[308,2801,398],{"class":321},[308,2803,2804],{"class":325}," createOrderResponse",[308,2806,494],{"class":321},[308,2808,497],{"class":342},[308,2810,2811,2813,2815],{"class":310,"line":679},[308,2812,1968],{"class":342},[308,2814,933],{"class":321},[308,2816,2817],{"class":349}," `json:\"order_id\"`\n",[308,2819,2820,2822,2824],{"class":310,"line":696},[308,2821,1975],{"class":342},[308,2823,524],{"class":321},[308,2825,2826],{"class":349},"  `json:\"total\"`\n",[308,2828,2829],{"class":310,"line":708},[308,2830,533],{"class":342},[308,2832,2833],{"class":310,"line":714},[308,2834,333],{"emptyLinePlaceholder":332},[308,2836,2837,2839,2841,2844,2846,2848,2850,2853,2855,2857,2860,2862,2864,2866,2868],{"class":310,"line":732},[308,2838,623],{"class":321},[308,2840,914],{"class":342},[308,2842,2843],{"class":632},"h ",[308,2845,666],{"class":321},[308,2847,150],{"class":325},[308,2849,924],{"class":342},[308,2851,2852],{"class":325},"Create",[308,2854,629],{"class":342},[308,2856,2650],{"class":632},[308,2858,2859],{"class":325}," echo",[308,2861,604],{"class":342},[308,2863,1661],{"class":325},[308,2865,924],{"class":342},[308,2867,673],{"class":321},[308,2869,497],{"class":342},[308,2871,2872,2874,2877],{"class":310,"line":742},[308,2873,750],{"class":321},[308,2875,2876],{"class":342}," req ",[308,2878,2879],{"class":325},"createOrderRequest\n",[308,2881,2882,2884,2886,2888,2891,2894,2896,2899,2902,2904,2906],{"class":310,"line":747},[308,2883,682],{"class":321},[308,2885,2230],{"class":342},[308,2887,767],{"class":321},[308,2889,2890],{"class":342}," c.",[308,2892,2893],{"class":325},"Bind",[308,2895,629],{"class":342},[308,2897,2898],{"class":321},"&",[308,2900,2901],{"class":342},"req); err ",[308,2903,1154],{"class":321},[308,2905,702],{"class":423},[308,2907,497],{"class":342},[308,2909,2910,2912,2914,2917,2920,2923,2926,2929],{"class":310,"line":758},[308,2911,699],{"class":321},[308,2913,2890],{"class":342},[308,2915,2916],{"class":325},"JSON",[308,2918,2919],{"class":342},"(http.StatusBadRequest, ",[308,2921,2922],{"class":325},"errorResp",[308,2924,2925],{"class":342},"{Error: ",[308,2927,2928],{"class":349},"\"invalid body\"",[308,2930,2931],{"class":342},"})\n",[308,2933,2934],{"class":310,"line":776},[308,2935,711],{"class":342},[308,2937,2938,2940,2942,2944,2946,2949,2951,2953,2955,2957,2959],{"class":310,"line":792},[308,2939,682],{"class":321},[308,2941,2230],{"class":342},[308,2943,767],{"class":321},[308,2945,2890],{"class":342},[308,2947,2948],{"class":325},"Validate",[308,2950,629],{"class":342},[308,2952,2898],{"class":321},[308,2954,2901],{"class":342},[308,2956,1154],{"class":321},[308,2958,702],{"class":423},[308,2960,497],{"class":342},[308,2962,2963,2965,2967,2969,2971,2973,2976,2979],{"class":310,"line":803},[308,2964,699],{"class":321},[308,2966,2890],{"class":342},[308,2968,2916],{"class":325},[308,2970,2919],{"class":342},[308,2972,2922],{"class":325},[308,2974,2975],{"class":342},"{Error: err.",[308,2977,2978],{"class":325},"Error",[308,2980,2375],{"class":342},[308,2982,2983],{"class":310,"line":809},[308,2984,711],{"class":342},[308,2986,2987],{"class":310,"line":829},[308,2988,333],{"emptyLinePlaceholder":332},[308,2990,2991,2993,2995,2997,2999,3001,3003,3006,3008,3010,3012,3014],{"class":310,"line":834},[308,2992,2134],{"class":342},[308,2994,767],{"class":321},[308,2996,2139],{"class":325},[308,2998,1006],{"class":342},[308,3000,2597],{"class":325},[308,3002,604],{"class":342},[308,3004,3005],{"class":325},"CreateOrderItem",[308,3007,80],{"class":342},[308,3009,2152],{"class":423},[308,3011,80],{"class":342},[308,3013,2157],{"class":325},[308,3015,3016],{"class":342},"(req.Items))\n",[308,3018,3019,3021,3023,3025,3027],{"class":310,"line":848},[308,3020,761],{"class":321},[308,3022,764],{"class":342},[308,3024,767],{"class":321},[308,3026,770],{"class":321},[308,3028,3029],{"class":342}," req.Items {\n",[308,3031,3032,3034,3036,3038,3040,3042,3044,3046],{"class":310,"line":860},[308,3033,2178],{"class":342},[308,3035,1179],{"class":321},[308,3037,1003],{"class":325},[308,3039,2185],{"class":342},[308,3041,2597],{"class":325},[308,3043,604],{"class":342},[308,3045,3005],{"class":325},[308,3047,2194],{"class":342},[308,3049,3050],{"class":310,"line":866},[308,3051,711],{"class":342},[308,3053,3054],{"class":310,"line":872},[308,3055,333],{"emptyLinePlaceholder":332},[308,3057,3058,3061,3063,3066,3068,3071,3074,3077,3079,3081,3083,3085,3088],{"class":310,"line":878},[308,3059,3060],{"class":342},"    out, err ",[308,3062,767],{"class":321},[308,3064,3065],{"class":342}," h.create.",[308,3067,2101],{"class":325},[308,3069,3070],{"class":342},"(c.",[308,3072,3073],{"class":325},"Request",[308,3075,3076],{"class":342},"().",[308,3078,1661],{"class":325},[308,3080,2365],{"class":342},[308,3082,2597],{"class":325},[308,3084,604],{"class":342},[308,3086,3087],{"class":325},"CreateOrderInput",[308,3089,845],{"class":342},[308,3091,3092],{"class":310,"line":884},[308,3093,3094],{"class":342},"        UserID: req.UserID,\n",[308,3096,3097],{"class":310,"line":890},[308,3098,3099],{"class":342},"        Items:  items,\n",[308,3101,3102],{"class":310,"line":899},[308,3103,3104],{"class":342},"    })\n",[308,3106,3107,3109,3111,3113,3115],{"class":310,"line":904},[308,3108,682],{"class":321},[308,3110,2230],{"class":342},[308,3112,1154],{"class":321},[308,3114,702],{"class":423},[308,3116,497],{"class":342},[308,3118,3119,3121,3124],{"class":310,"line":909},[308,3120,699],{"class":321},[308,3122,3123],{"class":325}," mapError",[308,3125,3126],{"class":342},"(c, err)\n",[308,3128,3129],{"class":310,"line":945},[308,3130,711],{"class":342},[308,3132,3133],{"class":310,"line":975},[308,3134,333],{"emptyLinePlaceholder":332},[308,3136,3137,3139,3141,3143,3146,3149],{"class":310,"line":1025},[308,3138,837],{"class":321},[308,3140,2890],{"class":342},[308,3142,2916],{"class":325},[308,3144,3145],{"class":342},"(http.StatusCreated, ",[308,3147,3148],{"class":325},"createOrderResponse",[308,3150,845],{"class":342},[308,3152,3153],{"class":310,"line":1053},[308,3154,3155],{"class":342},"        OrderID: out.OrderID,\n",[308,3157,3158],{"class":310,"line":1083},[308,3159,3160],{"class":342},"        Total:   out.Total,\n",[308,3162,3163],{"class":310,"line":1117},[308,3164,3104],{"class":342},[308,3166,3167],{"class":310,"line":1122},[308,3168,533],{"class":342},[38,3170,3172],{"className":302,"code":3171,"language":304,"meta":5,"style":5},"\u002F\u002F internal\u002Fadapter\u002Fhttp\u002Ferror_mapper.go\npackage httpadapter\n\nimport (\n    \"errors\"\n    \"net\u002Fhttp\"\n\n    \"github.com\u002Flabstack\u002Fecho\u002Fv4\"\n\n    \"myproject\u002Finternal\u002Fdomain\u002Forder\"\n)\n\ntype errorResp struct {\n    Error string `json:\"error\"`\n}\n\nfunc mapError(c echo.Context, err error) error {\n    switch {\n    case errors.Is(err, order.ErrNotFound):\n        return c.JSON(http.StatusNotFound, errorResp{Error: \"not found\"})\n    case errors.Is(err, order.ErrEmptyItems),\n        errors.Is(err, order.ErrInvalidQuantity),\n        errors.Is(err, order.ErrEmptyUser):\n        return c.JSON(http.StatusBadRequest, errorResp{Error: err.Error()})\n    case errors.Is(err, order.ErrAlreadyShipped),\n        errors.Is(err, order.ErrInvalidTransition):\n        return c.JSON(http.StatusConflict, errorResp{Error: err.Error()})\n    default:\n        c.Logger().Error(err)\n        return c.JSON(http.StatusInternalServerError, errorResp{Error: \"internal\"})\n    }\n}\n",[20,3173,3174,3179,3185,3189,3195,3203,3211,3215,3223,3227,3235,3239,3243,3254,3264,3268,3272,3302,3309,3322,3342,3353,3363,3372,3390,3401,3410,3429,3437,3451,3471,3475],{"__ignoreMap":5},[308,3175,3176],{"class":310,"line":311},[308,3177,3178],{"class":314},"\u002F\u002F internal\u002Fadapter\u002Fhttp\u002Ferror_mapper.go\n",[308,3180,3181,3183],{"class":310,"line":318},[308,3182,322],{"class":321},[308,3184,2507],{"class":325},[308,3186,3187],{"class":310,"line":329},[308,3188,333],{"emptyLinePlaceholder":332},[308,3190,3191,3193],{"class":310,"line":336},[308,3192,339],{"class":321},[308,3194,343],{"class":342},[308,3196,3197,3199,3201],{"class":310,"line":346},[308,3198,350],{"class":349},[308,3200,353],{"class":325},[308,3202,356],{"class":349},[308,3204,3205,3207,3209],{"class":310,"line":359},[308,3206,350],{"class":349},[308,3208,2532],{"class":325},[308,3210,356],{"class":349},[308,3212,3213],{"class":310,"line":369},[308,3214,333],{"emptyLinePlaceholder":332},[308,3216,3217,3219,3221],{"class":310,"line":374},[308,3218,350],{"class":349},[308,3220,2545],{"class":325},[308,3222,356],{"class":349},[308,3224,3225],{"class":310,"line":384},[308,3226,333],{"emptyLinePlaceholder":332},[308,3228,3229,3231,3233],{"class":310,"line":390},[308,3230,350],{"class":349},[308,3232,1622],{"class":325},[308,3234,356],{"class":349},[308,3236,3237],{"class":310,"line":395},[308,3238,387],{"class":342},[308,3240,3241],{"class":310,"line":407},[308,3242,333],{"emptyLinePlaceholder":332},[308,3244,3245,3247,3250,3252],{"class":310,"line":412},[308,3246,398],{"class":321},[308,3248,3249],{"class":325}," errorResp",[308,3251,494],{"class":321},[308,3253,497],{"class":342},[308,3255,3256,3259,3261],{"class":310,"line":420},[308,3257,3258],{"class":342},"    Error ",[308,3260,933],{"class":321},[308,3262,3263],{"class":349}," `json:\"error\"`\n",[308,3265,3266],{"class":310,"line":436},[308,3267,533],{"class":342},[308,3269,3270],{"class":310,"line":449},[308,3271,333],{"emptyLinePlaceholder":332},[308,3273,3274,3276,3278,3280,3282,3284,3286,3288,3290,3293,3296,3298,3300],{"class":310,"line":463},[308,3275,623],{"class":321},[308,3277,3123],{"class":325},[308,3279,629],{"class":342},[308,3281,2650],{"class":632},[308,3283,2859],{"class":325},[308,3285,604],{"class":342},[308,3287,1661],{"class":325},[308,3289,80],{"class":342},[308,3291,3292],{"class":632},"err",[308,3294,3295],{"class":321}," error",[308,3297,924],{"class":342},[308,3299,673],{"class":321},[308,3301,497],{"class":342},[308,3303,3304,3307],{"class":310,"line":476},[308,3305,3306],{"class":321},"    switch",[308,3308,497],{"class":342},[308,3310,3311,3314,3316,3319],{"class":310,"line":481},[308,3312,3313],{"class":321},"    case",[308,3315,1436],{"class":342},[308,3317,3318],{"class":325},"Is",[308,3320,3321],{"class":342},"(err, order.ErrNotFound):\n",[308,3323,3324,3326,3328,3330,3333,3335,3337,3340],{"class":310,"line":486},[308,3325,699],{"class":321},[308,3327,2890],{"class":342},[308,3329,2916],{"class":325},[308,3331,3332],{"class":342},"(http.StatusNotFound, ",[308,3334,2922],{"class":325},[308,3336,2925],{"class":342},[308,3338,3339],{"class":349},"\"not found\"",[308,3341,2931],{"class":342},[308,3343,3344,3346,3348,3350],{"class":310,"line":500},[308,3345,3313],{"class":321},[308,3347,1436],{"class":342},[308,3349,3318],{"class":325},[308,3351,3352],{"class":342},"(err, order.ErrEmptyItems),\n",[308,3354,3355,3358,3360],{"class":310,"line":509},[308,3356,3357],{"class":342},"        errors.",[308,3359,3318],{"class":325},[308,3361,3362],{"class":342},"(err, order.ErrInvalidQuantity),\n",[308,3364,3365,3367,3369],{"class":310,"line":518},[308,3366,3357],{"class":342},[308,3368,3318],{"class":325},[308,3370,3371],{"class":342},"(err, order.ErrEmptyUser):\n",[308,3373,3374,3376,3378,3380,3382,3384,3386,3388],{"class":310,"line":530},[308,3375,699],{"class":321},[308,3377,2890],{"class":342},[308,3379,2916],{"class":325},[308,3381,2919],{"class":342},[308,3383,2922],{"class":325},[308,3385,2975],{"class":342},[308,3387,2978],{"class":325},[308,3389,2375],{"class":342},[308,3391,3392,3394,3396,3398],{"class":310,"line":536},[308,3393,3313],{"class":321},[308,3395,1436],{"class":342},[308,3397,3318],{"class":325},[308,3399,3400],{"class":342},"(err, order.ErrAlreadyShipped),\n",[308,3402,3403,3405,3407],{"class":310,"line":541},[308,3404,3357],{"class":342},[308,3406,3318],{"class":325},[308,3408,3409],{"class":342},"(err, order.ErrInvalidTransition):\n",[308,3411,3412,3414,3416,3418,3421,3423,3425,3427],{"class":310,"line":553},[308,3413,699],{"class":321},[308,3415,2890],{"class":342},[308,3417,2916],{"class":325},[308,3419,3420],{"class":342},"(http.StatusConflict, ",[308,3422,2922],{"class":325},[308,3424,2975],{"class":342},[308,3426,2978],{"class":325},[308,3428,2375],{"class":342},[308,3430,3431,3434],{"class":310,"line":561},[308,3432,3433],{"class":321},"    default",[308,3435,3436],{"class":342},":\n",[308,3438,3439,3442,3444,3446,3448],{"class":310,"line":569},[308,3440,3441],{"class":342},"        c.",[308,3443,165],{"class":325},[308,3445,3076],{"class":342},[308,3447,2978],{"class":325},[308,3449,3450],{"class":342},"(err)\n",[308,3452,3453,3455,3457,3459,3462,3464,3466,3469],{"class":310,"line":578},[308,3454,699],{"class":321},[308,3456,2890],{"class":342},[308,3458,2916],{"class":325},[308,3460,3461],{"class":342},"(http.StatusInternalServerError, ",[308,3463,2922],{"class":325},[308,3465,2925],{"class":342},[308,3467,3468],{"class":349},"\"internal\"",[308,3470,2931],{"class":342},[308,3472,3473],{"class":310,"line":587},[308,3474,711],{"class":342},[308,3476,3477],{"class":310,"line":596},[308,3478,533],{"class":342},[33,3480,3482,3483],{"id":3481},"_4-repository-postgresorderrepo","4. Repository — ",[20,3484,135],{},[38,3486,3488],{"className":302,"code":3487,"language":304,"meta":5,"style":5},"\u002F\u002F internal\u002Fadapter\u002Fpostgres\u002Forder_repo.go\npackage postgres\n\nimport (\n    \"context\"\n    \"encoding\u002Fjson\"\n    \"errors\"\n    \"fmt\"\n    \"time\"\n\n    \"github.com\u002Fjackc\u002Fpgx\u002Fv5\"\n    \"github.com\u002Fjackc\u002Fpgx\u002Fv5\u002Fpgxpool\"\n\n    \"myproject\u002Finternal\u002Fdomain\u002Forder\"\n)\n\ntype OrderRepo struct {\n    pool *pgxpool.Pool\n}\n\nfunc NewOrderRepo(pool *pgxpool.Pool) *OrderRepo {\n    return &OrderRepo{pool: pool}\n}\n\nfunc (r *OrderRepo) Save(ctx context.Context, o *order.Order) error {\n    items, err := json.Marshal(o.Items())\n    if err != nil {\n        return fmt.Errorf(\"marshal order items: %w\", err)\n    }\n\n    \u002F\u002F получаем конкретного executor (Tx из контекста или pool)\n    db := executor(ctx, r.pool)\n\n    _, err = db.Exec(ctx, `\n        INSERT INTO orders (id, user_id, items, status, total, created_at)\n        VALUES ($1, $2, $3, $4, $5, $6)\n        ON CONFLICT (id) DO UPDATE\n        SET items = EXCLUDED.items, status = EXCLUDED.status, total = EXCLUDED.total\n    `, o.ID(), o.UserID(), items, o.Status(), o.Total(), o.CreatedAt())\n\n    return err\n}\n\nfunc (r *OrderRepo) FindByID(ctx context.Context, id string) (*order.Order, error) {\n    db := executor(ctx, r.pool)\n\n    var (\n        userID    string\n        rawItems  []byte\n        status    order.Status\n        total     int64\n        createdAt time.Time\n    )\n\n    row := db.QueryRow(ctx, `\n        SELECT user_id, items, status, total, created_at\n        FROM orders WHERE id = $1\n    `, id)\n\n    if err := row.Scan(&userID, &rawItems, &status, &total, &createdAt); err != nil {\n        if errors.Is(err, pgx.ErrNoRows) {\n            return nil, order.ErrNotFound\n        }\n        return nil, err\n    }\n\n    var items []order.Item\n    if err := json.Unmarshal(rawItems, &items); err != nil {\n        return nil, fmt.Errorf(\"unmarshal order items: %w\", err)\n    }\n\n    return order.Hydrate(id, userID, items, status, total, createdAt), nil\n}\n",[20,3489,3490,3495,3502,3506,3512,3520,3528,3536,3544,3552,3556,3565,3574,3578,3586,3590,3594,3605,3620,3624,3628,3658,3669,3673,3677,3722,3740,3752,3772,3776,3780,3785,3798,3802,3820,3825,3830,3835,3840,3870,3874,3881,3885,3889,3938,3948,3952,3958,3965,3973,3984,3991,4002,4007,4011,4027,4032,4037,4044,4048,4095,4106,4115,4119,4128,4132,4136,4149,4176,4198,4202,4206,4220],{"__ignoreMap":5},[308,3491,3492],{"class":310,"line":311},[308,3493,3494],{"class":314},"\u002F\u002F internal\u002Fadapter\u002Fpostgres\u002Forder_repo.go\n",[308,3496,3497,3499],{"class":310,"line":318},[308,3498,322],{"class":321},[308,3500,3501],{"class":325}," postgres\n",[308,3503,3504],{"class":310,"line":329},[308,3505,333],{"emptyLinePlaceholder":332},[308,3507,3508,3510],{"class":310,"line":336},[308,3509,339],{"class":321},[308,3511,343],{"class":342},[308,3513,3514,3516,3518],{"class":310,"line":346},[308,3515,350],{"class":349},[308,3517,1601],{"class":325},[308,3519,356],{"class":349},[308,3521,3522,3524,3526],{"class":310,"line":359},[308,3523,350],{"class":349},[308,3525,1850],{"class":325},[308,3527,356],{"class":349},[308,3529,3530,3532,3534],{"class":310,"line":369},[308,3531,350],{"class":349},[308,3533,353],{"class":325},[308,3535,356],{"class":349},[308,3537,3538,3540,3542],{"class":310,"line":374},[308,3539,350],{"class":349},[308,3541,1859],{"class":325},[308,3543,356],{"class":349},[308,3545,3546,3548,3550],{"class":310,"line":384},[308,3547,350],{"class":349},[308,3549,364],{"class":325},[308,3551,356],{"class":349},[308,3553,3554],{"class":310,"line":390},[308,3555,333],{"emptyLinePlaceholder":332},[308,3557,3558,3560,3563],{"class":310,"line":395},[308,3559,350],{"class":349},[308,3561,3562],{"class":325},"github.com\u002Fjackc\u002Fpgx\u002Fv5",[308,3564,356],{"class":349},[308,3566,3567,3569,3572],{"class":310,"line":407},[308,3568,350],{"class":349},[308,3570,3571],{"class":325},"github.com\u002Fjackc\u002Fpgx\u002Fv5\u002Fpgxpool",[308,3573,356],{"class":349},[308,3575,3576],{"class":310,"line":412},[308,3577,333],{"emptyLinePlaceholder":332},[308,3579,3580,3582,3584],{"class":310,"line":420},[308,3581,350],{"class":349},[308,3583,1622],{"class":325},[308,3585,356],{"class":349},[308,3587,3588],{"class":310,"line":436},[308,3589,387],{"class":342},[308,3591,3592],{"class":310,"line":449},[308,3593,333],{"emptyLinePlaceholder":332},[308,3595,3596,3598,3601,3603],{"class":310,"line":463},[308,3597,398],{"class":321},[308,3599,3600],{"class":325}," OrderRepo",[308,3602,494],{"class":321},[308,3604,497],{"class":342},[308,3606,3607,3610,3612,3615,3617],{"class":310,"line":476},[308,3608,3609],{"class":342},"    pool ",[308,3611,666],{"class":321},[308,3613,3614],{"class":325},"pgxpool",[308,3616,604],{"class":342},[308,3618,3619],{"class":325},"Pool\n",[308,3621,3622],{"class":310,"line":481},[308,3623,533],{"class":342},[308,3625,3626],{"class":310,"line":486},[308,3627,333],{"emptyLinePlaceholder":332},[308,3629,3630,3632,3635,3637,3640,3642,3644,3646,3649,3651,3653,3656],{"class":310,"line":500},[308,3631,623],{"class":321},[308,3633,3634],{"class":325}," NewOrderRepo",[308,3636,629],{"class":342},[308,3638,3639],{"class":632},"pool",[308,3641,1669],{"class":321},[308,3643,3614],{"class":325},[308,3645,604],{"class":342},[308,3647,3648],{"class":325},"Pool",[308,3650,924],{"class":342},[308,3652,666],{"class":321},[308,3654,3655],{"class":325},"OrderRepo",[308,3657,497],{"class":342},[308,3659,3660,3662,3664,3666],{"class":310,"line":509},[308,3661,837],{"class":321},[308,3663,840],{"class":321},[308,3665,3655],{"class":325},[308,3667,3668],{"class":342},"{pool: pool}\n",[308,3670,3671],{"class":310,"line":518},[308,3672,533],{"class":342},[308,3674,3675],{"class":310,"line":530},[308,3676,333],{"emptyLinePlaceholder":332},[308,3678,3679,3681,3683,3686,3688,3690,3692,3694,3696,3698,3700,3702,3704,3706,3708,3710,3712,3714,3716,3718,3720],{"class":310,"line":536},[308,3680,623],{"class":321},[308,3682,914],{"class":342},[308,3684,3685],{"class":632},"r ",[308,3687,666],{"class":321},[308,3689,3655],{"class":325},[308,3691,924],{"class":342},[308,3693,2284],{"class":325},[308,3695,629],{"class":342},[308,3697,1653],{"class":632},[308,3699,1656],{"class":325},[308,3701,604],{"class":342},[308,3703,1661],{"class":325},[308,3705,80],{"class":342},[308,3707,1666],{"class":632},[308,3709,1669],{"class":321},[308,3711,1672],{"class":325},[308,3713,604],{"class":342},[308,3715,79],{"class":325},[308,3717,924],{"class":342},[308,3719,673],{"class":321},[308,3721,497],{"class":342},[308,3723,3724,3727,3729,3731,3733,3736,3738],{"class":310,"line":541},[308,3725,3726],{"class":342},"    items, err ",[308,3728,767],{"class":321},[308,3730,2332],{"class":342},[308,3732,2335],{"class":325},[308,3734,3735],{"class":342},"(o.",[308,3737,990],{"class":325},[308,3739,2223],{"class":342},[308,3741,3742,3744,3746,3748,3750],{"class":310,"line":553},[308,3743,682],{"class":321},[308,3745,2230],{"class":342},[308,3747,1154],{"class":321},[308,3749,702],{"class":423},[308,3751,497],{"class":342},[308,3753,3754,3756,3759,3761,3763,3766,3768,3770],{"class":310,"line":561},[308,3755,699],{"class":321},[308,3757,3758],{"class":342}," fmt.",[308,3760,2248],{"class":325},[308,3762,629],{"class":342},[308,3764,3765],{"class":349},"\"marshal order items: ",[308,3767,2256],{"class":423},[308,3769,2259],{"class":349},[308,3771,2262],{"class":342},[308,3773,3774],{"class":310,"line":569},[308,3775,711],{"class":342},[308,3777,3778],{"class":310,"line":578},[308,3779,333],{"emptyLinePlaceholder":332},[308,3781,3782],{"class":310,"line":587},[308,3783,3784],{"class":314},"    \u002F\u002F получаем конкретного executor (Tx из контекста или pool)\n",[308,3786,3787,3790,3792,3795],{"class":310,"line":596},[308,3788,3789],{"class":342},"    db ",[308,3791,767],{"class":321},[308,3793,3794],{"class":325}," executor",[308,3796,3797],{"class":342},"(ctx, r.pool)\n",[308,3799,3800],{"class":310,"line":610},[308,3801,333],{"emptyLinePlaceholder":332},[308,3803,3804,3807,3809,3812,3815,3817],{"class":310,"line":615},[308,3805,3806],{"class":342},"    _, err ",[308,3808,1179],{"class":321},[308,3810,3811],{"class":342}," db.",[308,3813,3814],{"class":325},"Exec",[308,3816,2429],{"class":342},[308,3818,3819],{"class":349},"`\n",[308,3821,3822],{"class":310,"line":620},[308,3823,3824],{"class":349},"        INSERT INTO orders (id, user_id, items, status, total, created_at)\n",[308,3826,3827],{"class":310,"line":679},[308,3828,3829],{"class":349},"        VALUES ($1, $2, $3, $4, $5, $6)\n",[308,3831,3832],{"class":310,"line":696},[308,3833,3834],{"class":349},"        ON CONFLICT (id) DO UPDATE\n",[308,3836,3837],{"class":310,"line":708},[308,3838,3839],{"class":349},"        SET items = EXCLUDED.items, status = EXCLUDED.status, total = EXCLUDED.total\n",[308,3841,3842,3845,3848,3850,3853,3855,3858,3860,3862,3864,3866,3868],{"class":310,"line":714},[308,3843,3844],{"class":349},"    `",[308,3846,3847],{"class":342},", o.",[308,3849,927],{"class":325},[308,3851,3852],{"class":342},"(), o.",[308,3854,960],{"class":325},[308,3856,3857],{"class":342},"(), items, o.",[308,3859,86],{"class":325},[308,3861,3852],{"class":342},[308,3863,1068],{"class":325},[308,3865,3852],{"class":342},[308,3867,1098],{"class":325},[308,3869,2223],{"class":342},[308,3871,3872],{"class":310,"line":732},[308,3873,333],{"emptyLinePlaceholder":332},[308,3875,3876,3878],{"class":310,"line":742},[308,3877,837],{"class":321},[308,3879,3880],{"class":342}," err\n",[308,3882,3883],{"class":310,"line":747},[308,3884,533],{"class":342},[308,3886,3887],{"class":310,"line":758},[308,3888,333],{"emptyLinePlaceholder":332},[308,3890,3891,3893,3895,3897,3899,3901,3903,3906,3908,3910,3912,3914,3916,3918,3920,3922,3924,3926,3928,3930,3932,3934,3936],{"class":310,"line":776},[308,3892,623],{"class":321},[308,3894,914],{"class":342},[308,3896,3685],{"class":632},[308,3898,666],{"class":321},[308,3900,3655],{"class":325},[308,3902,924],{"class":342},[308,3904,3905],{"class":325},"FindByID",[308,3907,629],{"class":342},[308,3909,1653],{"class":632},[308,3911,1656],{"class":325},[308,3913,604],{"class":342},[308,3915,1661],{"class":325},[308,3917,80],{"class":342},[308,3919,1320],{"class":632},[308,3921,636],{"class":321},[308,3923,663],{"class":342},[308,3925,666],{"class":321},[308,3927,1672],{"class":325},[308,3929,604],{"class":342},[308,3931,79],{"class":325},[308,3933,80],{"class":342},[308,3935,673],{"class":321},[308,3937,676],{"class":342},[308,3939,3940,3942,3944,3946],{"class":310,"line":792},[308,3941,3789],{"class":342},[308,3943,767],{"class":321},[308,3945,3794],{"class":325},[308,3947,3797],{"class":342},[308,3949,3950],{"class":310,"line":803},[308,3951,333],{"emptyLinePlaceholder":332},[308,3953,3954,3956],{"class":310,"line":809},[308,3955,750],{"class":321},[308,3957,343],{"class":342},[308,3959,3960,3963],{"class":310,"line":829},[308,3961,3962],{"class":342},"        userID    ",[308,3964,506],{"class":321},[308,3966,3967,3970],{"class":310,"line":834},[308,3968,3969],{"class":342},"        rawItems  []",[308,3971,3972],{"class":321},"byte\n",[308,3974,3975,3978,3980,3982],{"class":310,"line":848},[308,3976,3977],{"class":342},"        status    ",[308,3979,1672],{"class":325},[308,3981,604],{"class":342},[308,3983,584],{"class":325},[308,3985,3986,3989],{"class":310,"line":860},[308,3987,3988],{"class":342},"        total     ",[308,3990,593],{"class":321},[308,3992,3993,3996,3998,4000],{"class":310,"line":866},[308,3994,3995],{"class":342},"        createdAt ",[308,3997,364],{"class":325},[308,3999,604],{"class":342},[308,4001,607],{"class":325},[308,4003,4004],{"class":310,"line":872},[308,4005,4006],{"class":342},"    )\n",[308,4008,4009],{"class":310,"line":878},[308,4010,333],{"emptyLinePlaceholder":332},[308,4012,4013,4016,4018,4020,4023,4025],{"class":310,"line":884},[308,4014,4015],{"class":342},"    row ",[308,4017,767],{"class":321},[308,4019,3811],{"class":342},[308,4021,4022],{"class":325},"QueryRow",[308,4024,2429],{"class":342},[308,4026,3819],{"class":349},[308,4028,4029],{"class":310,"line":890},[308,4030,4031],{"class":349},"        SELECT user_id, items, status, total, created_at\n",[308,4033,4034],{"class":310,"line":899},[308,4035,4036],{"class":349},"        FROM orders WHERE id = $1\n",[308,4038,4039,4041],{"class":310,"line":904},[308,4040,3844],{"class":349},[308,4042,4043],{"class":342},", id)\n",[308,4045,4046],{"class":310,"line":909},[308,4047,333],{"emptyLinePlaceholder":332},[308,4049,4050,4052,4054,4056,4059,4062,4064,4066,4069,4071,4074,4076,4079,4081,4084,4086,4089,4091,4093],{"class":310,"line":945},[308,4051,682],{"class":321},[308,4053,2230],{"class":342},[308,4055,767],{"class":321},[308,4057,4058],{"class":342}," row.",[308,4060,4061],{"class":325},"Scan",[308,4063,629],{"class":342},[308,4065,2898],{"class":321},[308,4067,4068],{"class":342},"userID, ",[308,4070,2898],{"class":321},[308,4072,4073],{"class":342},"rawItems, ",[308,4075,2898],{"class":321},[308,4077,4078],{"class":342},"status, ",[308,4080,2898],{"class":321},[308,4082,4083],{"class":342},"total, ",[308,4085,2898],{"class":321},[308,4087,4088],{"class":342},"createdAt); err ",[308,4090,1154],{"class":321},[308,4092,702],{"class":423},[308,4094,497],{"class":342},[308,4096,4097,4099,4101,4103],{"class":310,"line":975},[308,4098,779],{"class":321},[308,4100,1436],{"class":342},[308,4102,3318],{"class":325},[308,4104,4105],{"class":342},"(err, pgx.ErrNoRows) {\n",[308,4107,4108,4110,4112],{"class":310,"line":1025},[308,4109,795],{"class":321},[308,4111,702],{"class":423},[308,4113,4114],{"class":342},", order.ErrNotFound\n",[308,4116,4117],{"class":310,"line":1053},[308,4118,806],{"class":342},[308,4120,4121,4123,4125],{"class":310,"line":1083},[308,4122,699],{"class":321},[308,4124,702],{"class":423},[308,4126,4127],{"class":342},", err\n",[308,4129,4130],{"class":310,"line":1117},[308,4131,711],{"class":342},[308,4133,4134],{"class":310,"line":1122},[308,4135,333],{"emptyLinePlaceholder":332},[308,4137,4138,4140,4143,4145,4147],{"class":310,"line":1146},[308,4139,750],{"class":321},[308,4141,4142],{"class":342}," items []",[308,4144,1672],{"class":325},[308,4146,604],{"class":342},[308,4148,575],{"class":325},[308,4150,4151,4153,4155,4157,4159,4162,4165,4167,4170,4172,4174],{"class":310,"line":1160},[308,4152,682],{"class":321},[308,4154,2230],{"class":342},[308,4156,767],{"class":321},[308,4158,2332],{"class":342},[308,4160,4161],{"class":325},"Unmarshal",[308,4163,4164],{"class":342},"(rawItems, ",[308,4166,2898],{"class":321},[308,4168,4169],{"class":342},"items); err ",[308,4171,1154],{"class":321},[308,4173,702],{"class":423},[308,4175,497],{"class":342},[308,4177,4178,4180,4182,4185,4187,4189,4192,4194,4196],{"class":310,"line":1168},[308,4179,699],{"class":321},[308,4181,702],{"class":423},[308,4183,4184],{"class":342},", fmt.",[308,4186,2248],{"class":325},[308,4188,629],{"class":342},[308,4190,4191],{"class":349},"\"unmarshal order items: ",[308,4193,2256],{"class":423},[308,4195,2259],{"class":349},[308,4197,2262],{"class":342},[308,4199,4200],{"class":310,"line":1173},[308,4201,711],{"class":342},[308,4203,4204],{"class":310,"line":1185},[308,4205,333],{"emptyLinePlaceholder":332},[308,4207,4208,4210,4212,4215,4218],{"class":310,"line":1193},[308,4209,837],{"class":321},[308,4211,2212],{"class":342},[308,4213,4214],{"class":325},"Hydrate",[308,4216,4217],{"class":342},"(id, userID, items, status, total, createdAt), ",[308,4219,896],{"class":423},[308,4221,4222],{"class":310,"line":1198},[308,4223,533],{"class":342},[16,4225,4226,4227,4230],{},"Repository возвращает ",[20,4228,4229],{},"*order.Order",", а не raw row. Вся «грязь» (JSON, scan, ON CONFLICT) живёт здесь и не утекает в use case.",[33,4232,4234,4235],{"id":4233},"_5-сборка-в-maingo","5. Сборка в ",[20,4236,4237],{},"main.go",[38,4239,4242],{"className":302,"code":4240,"language":304,"meta":4241,"style":5},"\u002F\u002F cmd\u002Fserver\u002Fmain.go\npackage main\n\nimport (\n    \"context\"\n    \"os\u002Fsignal\"\n    \"syscall\"\n    \"time\"\n\n    \"github.com\u002Fjackc\u002Fpgx\u002Fv5\u002Fpgxpool\"\n    \"github.com\u002Flabstack\u002Fecho\u002Fv4\"\n\n    \"myproject\u002Finternal\u002Fadapter\u002Fkafka\"\n    httpadapter \"myproject\u002Finternal\u002Fadapter\u002Fhttp\"\n    \"myproject\u002Finternal\u002Fadapter\u002Fpostgres\"\n    \"myproject\u002Finternal\u002Fconfig\"\n    \"myproject\u002Finternal\u002Fplatform\u002Flogger\"\n    \"myproject\u002Finternal\u002Fplatform\u002Fmetrics\"\n    \"myproject\u002Finternal\u002Fusecase\"\n)\n\nfunc main() {\n    cfg := config.Load()\n    log := logger.New(cfg.LogLevel)\n\n    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)\n    defer stop()\n\n    pool, err := pgxpool.New(ctx, cfg.DatabaseURL)\n    if err != nil {\n        log.Fatal(\"postgres connect\", \"err\", err)\n    }\n    defer pool.Close()\n\n    \u002F\u002F адаптеры\n    orderRepo := postgres.NewOrderRepo(pool)\n    txManager := postgres.NewTxManager(pool)\n    publisher := kafka.NewPublisher(cfg.KafkaBrokers)\n\n    \u002F\u002F use cases\n    createOrder := usecase.NewCreateOrder(orderRepo, publisher, realClock{})\n    cancelOrder := usecase.NewCancelOrder(orderRepo, txManager, publisher)\n    getOrder    := usecase.NewGetOrder(orderRepo)\n\n    \u002F\u002F handlers\n    orderHandler := httpadapter.NewOrderHandler(createOrder, cancelOrder, getOrder)\n\n    \u002F\u002F router\n    e := echo.New()\n    e.Use(metrics.Middleware(), logger.Middleware(log))\n    httpadapter.RegisterRoutes(e, orderHandler)\n\n    go func() {\n        if err := e.Start(cfg.HTTPAddr); err != nil {\n            log.Error(\"http server\", \"err\", err)\n        }\n    }()\n\n    \u003C-ctx.Done()\n    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n    defer cancel()\n    _ = e.Shutdown(shutdownCtx)\n}\n\ntype realClock struct{}\nfunc (realClock) Now() time.Time { return time.Now() }\n","no-run",[20,4243,4244,4249,4256,4260,4266,4274,4283,4292,4300,4304,4312,4320,4324,4333,4345,4354,4363,4372,4381,4389,4393,4397,4407,4423,4438,4442,4464,4474,4478,4493,4505,4525,4529,4541,4545,4550,4566,4580,4595,4599,4604,4626,4641,4656,4660,4665,4681,4685,4690,4704,4726,4737,4741,4751,4774,4792,4796,4801,4805,4818,4845,4854,4869,4873,4877,4889],{"__ignoreMap":5},[308,4245,4246],{"class":310,"line":311},[308,4247,4248],{"class":314},"\u002F\u002F cmd\u002Fserver\u002Fmain.go\n",[308,4250,4251,4253],{"class":310,"line":318},[308,4252,322],{"class":321},[308,4254,4255],{"class":325}," main\n",[308,4257,4258],{"class":310,"line":329},[308,4259,333],{"emptyLinePlaceholder":332},[308,4261,4262,4264],{"class":310,"line":336},[308,4263,339],{"class":321},[308,4265,343],{"class":342},[308,4267,4268,4270,4272],{"class":310,"line":346},[308,4269,350],{"class":349},[308,4271,1601],{"class":325},[308,4273,356],{"class":349},[308,4275,4276,4278,4281],{"class":310,"line":359},[308,4277,350],{"class":349},[308,4279,4280],{"class":325},"os\u002Fsignal",[308,4282,356],{"class":349},[308,4284,4285,4287,4290],{"class":310,"line":369},[308,4286,350],{"class":349},[308,4288,4289],{"class":325},"syscall",[308,4291,356],{"class":349},[308,4293,4294,4296,4298],{"class":310,"line":374},[308,4295,350],{"class":349},[308,4297,364],{"class":325},[308,4299,356],{"class":349},[308,4301,4302],{"class":310,"line":384},[308,4303,333],{"emptyLinePlaceholder":332},[308,4305,4306,4308,4310],{"class":310,"line":390},[308,4307,350],{"class":349},[308,4309,3571],{"class":325},[308,4311,356],{"class":349},[308,4313,4314,4316,4318],{"class":310,"line":395},[308,4315,350],{"class":349},[308,4317,2545],{"class":325},[308,4319,356],{"class":349},[308,4321,4322],{"class":310,"line":407},[308,4323,333],{"emptyLinePlaceholder":332},[308,4325,4326,4328,4331],{"class":310,"line":412},[308,4327,350],{"class":349},[308,4329,4330],{"class":325},"myproject\u002Finternal\u002Fadapter\u002Fkafka",[308,4332,356],{"class":349},[308,4334,4335,4338,4340,4343],{"class":310,"line":420},[308,4336,4337],{"class":342},"    httpadapter ",[308,4339,2259],{"class":349},[308,4341,4342],{"class":325},"myproject\u002Finternal\u002Fadapter\u002Fhttp",[308,4344,356],{"class":349},[308,4346,4347,4349,4352],{"class":310,"line":436},[308,4348,350],{"class":349},[308,4350,4351],{"class":325},"myproject\u002Finternal\u002Fadapter\u002Fpostgres",[308,4353,356],{"class":349},[308,4355,4356,4358,4361],{"class":310,"line":449},[308,4357,350],{"class":349},[308,4359,4360],{"class":325},"myproject\u002Finternal\u002Fconfig",[308,4362,356],{"class":349},[308,4364,4365,4367,4370],{"class":310,"line":463},[308,4366,350],{"class":349},[308,4368,4369],{"class":325},"myproject\u002Finternal\u002Fplatform\u002Flogger",[308,4371,356],{"class":349},[308,4373,4374,4376,4379],{"class":310,"line":476},[308,4375,350],{"class":349},[308,4377,4378],{"class":325},"myproject\u002Finternal\u002Fplatform\u002Fmetrics",[308,4380,356],{"class":349},[308,4382,4383,4385,4387],{"class":310,"line":481},[308,4384,350],{"class":349},[308,4386,2566],{"class":325},[308,4388,356],{"class":349},[308,4390,4391],{"class":310,"line":486},[308,4392,387],{"class":342},[308,4394,4395],{"class":310,"line":500},[308,4396,333],{"emptyLinePlaceholder":332},[308,4398,4399,4401,4404],{"class":310,"line":509},[308,4400,623],{"class":321},[308,4402,4403],{"class":325}," main",[308,4405,4406],{"class":342},"() {\n",[308,4408,4409,4412,4414,4417,4420],{"class":310,"line":518},[308,4410,4411],{"class":342},"    cfg ",[308,4413,767],{"class":321},[308,4415,4416],{"class":342}," config.",[308,4418,4419],{"class":325},"Load",[308,4421,4422],{"class":342},"()\n",[308,4424,4425,4428,4430,4433,4435],{"class":310,"line":530},[308,4426,4427],{"class":342},"    log ",[308,4429,767],{"class":321},[308,4431,4432],{"class":342}," logger.",[308,4434,1439],{"class":325},[308,4436,4437],{"class":342},"(cfg.LogLevel)\n",[308,4439,4440],{"class":310,"line":536},[308,4441,333],{"emptyLinePlaceholder":332},[308,4443,4444,4447,4449,4452,4455,4458,4461],{"class":310,"line":541},[308,4445,4446],{"class":342},"    ctx, stop ",[308,4448,767],{"class":321},[308,4450,4451],{"class":342}," signal.",[308,4453,4454],{"class":325},"NotifyContext",[308,4456,4457],{"class":342},"(context.",[308,4459,4460],{"class":325},"Background",[308,4462,4463],{"class":342},"(), syscall.SIGINT, syscall.SIGTERM)\n",[308,4465,4466,4469,4472],{"class":310,"line":553},[308,4467,4468],{"class":321},"    defer",[308,4470,4471],{"class":325}," stop",[308,4473,4422],{"class":342},[308,4475,4476],{"class":310,"line":561},[308,4477,333],{"emptyLinePlaceholder":332},[308,4479,4480,4483,4485,4488,4490],{"class":310,"line":569},[308,4481,4482],{"class":342},"    pool, err ",[308,4484,767],{"class":321},[308,4486,4487],{"class":342}," pgxpool.",[308,4489,1439],{"class":325},[308,4491,4492],{"class":342},"(ctx, cfg.DatabaseURL)\n",[308,4494,4495,4497,4499,4501,4503],{"class":310,"line":578},[308,4496,682],{"class":321},[308,4498,2230],{"class":342},[308,4500,1154],{"class":321},[308,4502,702],{"class":423},[308,4504,497],{"class":342},[308,4506,4507,4510,4513,4515,4518,4520,4523],{"class":310,"line":587},[308,4508,4509],{"class":342},"        log.",[308,4511,4512],{"class":325},"Fatal",[308,4514,629],{"class":342},[308,4516,4517],{"class":349},"\"postgres connect\"",[308,4519,80],{"class":342},[308,4521,4522],{"class":349},"\"err\"",[308,4524,2262],{"class":342},[308,4526,4527],{"class":310,"line":596},[308,4528,711],{"class":342},[308,4530,4531,4533,4536,4539],{"class":310,"line":610},[308,4532,4468],{"class":321},[308,4534,4535],{"class":342}," pool.",[308,4537,4538],{"class":325},"Close",[308,4540,4422],{"class":342},[308,4542,4543],{"class":310,"line":615},[308,4544,333],{"emptyLinePlaceholder":332},[308,4546,4547],{"class":310,"line":620},[308,4548,4549],{"class":314},"    \u002F\u002F адаптеры\n",[308,4551,4552,4555,4557,4560,4563],{"class":310,"line":679},[308,4553,4554],{"class":342},"    orderRepo ",[308,4556,767],{"class":321},[308,4558,4559],{"class":342}," postgres.",[308,4561,4562],{"class":325},"NewOrderRepo",[308,4564,4565],{"class":342},"(pool)\n",[308,4567,4568,4571,4573,4575,4578],{"class":310,"line":696},[308,4569,4570],{"class":342},"    txManager ",[308,4572,767],{"class":321},[308,4574,4559],{"class":342},[308,4576,4577],{"class":325},"NewTxManager",[308,4579,4565],{"class":342},[308,4581,4582,4584,4586,4589,4592],{"class":310,"line":708},[308,4583,2009],{"class":342},[308,4585,767],{"class":321},[308,4587,4588],{"class":342}," kafka.",[308,4590,4591],{"class":325},"NewPublisher",[308,4593,4594],{"class":342},"(cfg.KafkaBrokers)\n",[308,4596,4597],{"class":310,"line":714},[308,4598,333],{"emptyLinePlaceholder":332},[308,4600,4601],{"class":310,"line":732},[308,4602,4603],{"class":314},"    \u002F\u002F use cases\n",[308,4605,4606,4609,4611,4614,4617,4620,4623],{"class":310,"line":742},[308,4607,4608],{"class":342},"    createOrder ",[308,4610,767],{"class":321},[308,4612,4613],{"class":342}," usecase.",[308,4615,4616],{"class":325},"NewCreateOrder",[308,4618,4619],{"class":342},"(orderRepo, publisher, ",[308,4621,4622],{"class":325},"realClock",[308,4624,4625],{"class":342},"{})\n",[308,4627,4628,4631,4633,4635,4638],{"class":310,"line":747},[308,4629,4630],{"class":342},"    cancelOrder ",[308,4632,767],{"class":321},[308,4634,4613],{"class":342},[308,4636,4637],{"class":325},"NewCancelOrder",[308,4639,4640],{"class":342},"(orderRepo, txManager, publisher)\n",[308,4642,4643,4646,4648,4650,4653],{"class":310,"line":758},[308,4644,4645],{"class":342},"    getOrder    ",[308,4647,767],{"class":321},[308,4649,4613],{"class":342},[308,4651,4652],{"class":325},"NewGetOrder",[308,4654,4655],{"class":342},"(orderRepo)\n",[308,4657,4658],{"class":310,"line":776},[308,4659,333],{"emptyLinePlaceholder":332},[308,4661,4662],{"class":310,"line":792},[308,4663,4664],{"class":314},"    \u002F\u002F handlers\n",[308,4666,4667,4670,4672,4675,4678],{"class":310,"line":803},[308,4668,4669],{"class":342},"    orderHandler ",[308,4671,767],{"class":321},[308,4673,4674],{"class":342}," httpadapter.",[308,4676,4677],{"class":325},"NewOrderHandler",[308,4679,4680],{"class":342},"(createOrder, cancelOrder, getOrder)\n",[308,4682,4683],{"class":310,"line":809},[308,4684,333],{"emptyLinePlaceholder":332},[308,4686,4687],{"class":310,"line":829},[308,4688,4689],{"class":314},"    \u002F\u002F router\n",[308,4691,4692,4695,4697,4700,4702],{"class":310,"line":834},[308,4693,4694],{"class":342},"    e ",[308,4696,767],{"class":321},[308,4698,4699],{"class":342}," echo.",[308,4701,1439],{"class":325},[308,4703,4422],{"class":342},[308,4705,4706,4709,4712,4715,4718,4721,4723],{"class":310,"line":848},[308,4707,4708],{"class":342},"    e.",[308,4710,4711],{"class":325},"Use",[308,4713,4714],{"class":342},"(metrics.",[308,4716,4717],{"class":325},"Middleware",[308,4719,4720],{"class":342},"(), logger.",[308,4722,4717],{"class":325},[308,4724,4725],{"class":342},"(log))\n",[308,4727,4728,4731,4734],{"class":310,"line":860},[308,4729,4730],{"class":342},"    httpadapter.",[308,4732,4733],{"class":325},"RegisterRoutes",[308,4735,4736],{"class":342},"(e, orderHandler)\n",[308,4738,4739],{"class":310,"line":866},[308,4740,333],{"emptyLinePlaceholder":332},[308,4742,4743,4746,4749],{"class":310,"line":872},[308,4744,4745],{"class":321},"    go",[308,4747,4748],{"class":321}," func",[308,4750,4406],{"class":342},[308,4752,4753,4755,4757,4759,4762,4765,4768,4770,4772],{"class":310,"line":878},[308,4754,779],{"class":321},[308,4756,2230],{"class":342},[308,4758,767],{"class":321},[308,4760,4761],{"class":342}," e.",[308,4763,4764],{"class":325},"Start",[308,4766,4767],{"class":342},"(cfg.HTTPAddr); err ",[308,4769,1154],{"class":321},[308,4771,702],{"class":423},[308,4773,497],{"class":342},[308,4775,4776,4779,4781,4783,4786,4788,4790],{"class":310,"line":884},[308,4777,4778],{"class":342},"            log.",[308,4780,2978],{"class":325},[308,4782,629],{"class":342},[308,4784,4785],{"class":349},"\"http server\"",[308,4787,80],{"class":342},[308,4789,4522],{"class":349},[308,4791,2262],{"class":342},[308,4793,4794],{"class":310,"line":890},[308,4795,806],{"class":342},[308,4797,4798],{"class":310,"line":899},[308,4799,4800],{"class":342},"    }()\n",[308,4802,4803],{"class":310,"line":904},[308,4804,333],{"emptyLinePlaceholder":332},[308,4806,4807,4810,4813,4816],{"class":310,"line":909},[308,4808,4809],{"class":321},"    \u003C-",[308,4811,4812],{"class":342},"ctx.",[308,4814,4815],{"class":325},"Done",[308,4817,4422],{"class":342},[308,4819,4820,4823,4825,4828,4831,4833,4835,4837,4840,4842],{"class":310,"line":945},[308,4821,4822],{"class":342},"    shutdownCtx, cancel ",[308,4824,767],{"class":321},[308,4826,4827],{"class":342}," context.",[308,4829,4830],{"class":325},"WithTimeout",[308,4832,4457],{"class":342},[308,4834,4460],{"class":325},[308,4836,2365],{"class":342},[308,4838,4839],{"class":423},"10",[308,4841,666],{"class":321},[308,4843,4844],{"class":342},"time.Second)\n",[308,4846,4847,4849,4852],{"class":310,"line":975},[308,4848,4468],{"class":321},[308,4850,4851],{"class":325}," cancel",[308,4853,4422],{"class":342},[308,4855,4856,4859,4861,4863,4866],{"class":310,"line":1025},[308,4857,4858],{"class":342},"    _ ",[308,4860,1179],{"class":321},[308,4862,4761],{"class":342},[308,4864,4865],{"class":325},"Shutdown",[308,4867,4868],{"class":342},"(shutdownCtx)\n",[308,4870,4871],{"class":310,"line":1053},[308,4872,533],{"class":342},[308,4874,4875],{"class":310,"line":1083},[308,4876,333],{"emptyLinePlaceholder":332},[308,4878,4879,4881,4884,4886],{"class":310,"line":1117},[308,4880,398],{"class":321},[308,4882,4883],{"class":325}," realClock",[308,4885,494],{"class":321},[308,4887,4888],{"class":342},"{}\n",[308,4890,4891,4893,4895,4897,4899,4901,4903,4905,4907,4909,4911,4913,4916,4918],{"class":310,"line":1122},[308,4892,623],{"class":321},[308,4894,914],{"class":342},[308,4896,4622],{"class":325},[308,4898,924],{"class":342},[308,4900,2220],{"class":325},[308,4902,930],{"class":342},[308,4904,364],{"class":325},[308,4906,604],{"class":342},[308,4908,660],{"class":325},[308,4910,1109],{"class":342},[308,4912,939],{"class":321},[308,4914,4915],{"class":342}," time.",[308,4917,2220],{"class":325},[308,4919,4920],{"class":342},"() }\n",[16,4922,4923,4924,206,4927,4930],{},"DI руками. Никаких ",[20,4925,4926],{},"wire",[20,4928,4929],{},"fx"," — для проекта на 30 юзкейсов хватает четырёх десятков строк сборки.",[11,4932,4934],{"id":4933},"сквозные-concerns","Сквозные concerns",[33,4936,4938],{"id":4937},"ошибки-domain-vs-infrastructure","Ошибки: domain vs infrastructure",[16,4940,4941],{},"Два класса:",[38,4943,4945],{"className":302,"code":4944,"language":304,"meta":5,"style":5},"\u002F\u002F domain-ошибки — известные бизнес-ситуации, маппятся в HTTP-коды\norder.ErrNotFound          → 404\norder.ErrAlreadyShipped    → 409\norder.ErrInvalidQuantity   → 400\n\n\u002F\u002F infrastructure-ошибки — отказы внешнего мира\npgx.ErrNoRows              → транслируется в domain.ErrNotFound в repository\ncontext.DeadlineExceeded   → 504\nсетевая ошибка к Kafka     → 503 + retry\n",[20,4946,4947,4952,4960,4968,4976,4980,4985,4990,4998],{"__ignoreMap":5},[308,4948,4949],{"class":310,"line":311},[308,4950,4951],{"class":314},"\u002F\u002F domain-ошибки — известные бизнес-ситуации, маппятся в HTTP-коды\n",[308,4953,4954,4957],{"class":310,"line":318},[308,4955,4956],{"class":342},"order.ErrNotFound          → ",[308,4958,4959],{"class":423},"404\n",[308,4961,4962,4965],{"class":310,"line":329},[308,4963,4964],{"class":342},"order.ErrAlreadyShipped    → ",[308,4966,4967],{"class":423},"409\n",[308,4969,4970,4973],{"class":310,"line":336},[308,4971,4972],{"class":342},"order.ErrInvalidQuantity   → ",[308,4974,4975],{"class":423},"400\n",[308,4977,4978],{"class":310,"line":346},[308,4979,333],{"emptyLinePlaceholder":332},[308,4981,4982],{"class":310,"line":359},[308,4983,4984],{"class":314},"\u002F\u002F infrastructure-ошибки — отказы внешнего мира\n",[308,4986,4987],{"class":310,"line":369},[308,4988,4989],{"class":342},"pgx.ErrNoRows              → транслируется в domain.ErrNotFound в repository\n",[308,4991,4992,4995],{"class":310,"line":374},[308,4993,4994],{"class":342},"context.DeadlineExceeded   → ",[308,4996,4997],{"class":423},"504\n",[308,4999,5000,5003,5006,5009],{"class":310,"line":384},[308,5001,5002],{"class":342},"сетевая ошибка к Kafka     → ",[308,5004,5005],{"class":423},"503",[308,5007,5008],{"class":321}," +",[308,5010,5011],{"class":342}," retry\n",[16,5013,5014],{},"Правила:",[265,5016,5017,5023,5035],{},[268,5018,5019,5020],{},"Use case ловит infrastructure-ошибки и оборачивает в ",[20,5021,5022],{},"fmt.Errorf(\"...: %w\", err)",[268,5024,5025,5026,5030,5031,5034],{},"Repository ",[5027,5028,5029],"strong",{},"транслирует"," инфраструктурные в domain-ошибки (",[20,5032,5033],{},"pgx.ErrNoRows → order.ErrNotFound",")",[268,5036,5037,5038,5041],{},"Handler через ",[20,5039,5040],{},"errors.Is"," различает domain-ошибки и маппит в HTTP",[16,5043,5044,5045,5048,5049,206,5051,604],{},"Антипаттерн — ",[20,5046,5047],{},"if err.Error() == \"no rows in result set\"",". Только ",[20,5050,5040],{},[20,5052,5053],{},"errors.As",[33,5055,5057],{"id":5056},"contextcontext","Context.Context",[16,5059,5060,5062,5063,5066],{},[20,5061,1653],{}," пробрасывается ",[5027,5064,5065],{},"первым параметром"," на каждом слое — handler → use case → repository. Через него: дедлайны, отмена, request-id, span tracing, иногда транзакция.",[38,5068,5070],{"className":302,"code":5069,"language":304,"meta":5,"style":5},"ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)\ndefer cancel()\nout, err := h.create.Execute(ctx, in)\n",[20,5071,5072,5097,5106],{"__ignoreMap":5},[308,5073,5074,5077,5079,5081,5083,5086,5088,5090,5093,5095],{"class":310,"line":311},[308,5075,5076],{"class":342},"ctx, cancel ",[308,5078,767],{"class":321},[308,5080,4827],{"class":342},[308,5082,4830],{"class":325},[308,5084,5085],{"class":342},"(r.",[308,5087,1661],{"class":325},[308,5089,2365],{"class":342},[308,5091,5092],{"class":423},"3",[308,5094,666],{"class":321},[308,5096,4844],{"class":342},[308,5098,5099,5102,5104],{"class":310,"line":318},[308,5100,5101],{"class":321},"defer",[308,5103,4851],{"class":325},[308,5105,4422],{"class":342},[308,5107,5108,5111,5113,5115,5117],{"class":310,"line":329},[308,5109,5110],{"class":342},"out, err ",[308,5112,767],{"class":321},[308,5114,3065],{"class":342},[308,5116,2101],{"class":325},[308,5118,5119],{"class":342},"(ctx, in)\n",[16,5121,5122],{},"Чего НЕ делать с context:",[265,5124,5125,5135,5138],{},[268,5126,5127,5128,5131,5132,5134],{},"Не класть в ",[20,5129,5130],{},"context.Value"," бизнес-данные (",[20,5133,960],{}," лучше через явный параметр)",[268,5136,5137],{},"Не хранить ctx в полях структуры",[268,5139,5140,5141,5144],{},"Не игнорировать ",[20,5142,5143],{},"ctx.Err()"," в долгих циклах",[33,5146,5148],{"id":5147},"логирование-где-и-как","Логирование: где и как",[38,5150,5153],{"className":5151,"code":5152,"language":43,"meta":5},[41],"domain   — НЕ логирует. Возвращает ошибки.\nusecase  — логирует ВАЖНЫЕ бизнес-события (Info: \"order created\"), ошибки оркестрации.\nadapter  — логирует технические детали (slow query, retry, transport errors).\n",[20,5154,5152],{"__ignoreMap":5},[16,5156,5157,5158,5160],{},"Logger пробрасывается через DI или достаётся из ",[20,5159,1653],{}," (если используется request-scoped logger):",[38,5162,5164],{"className":302,"code":5163,"language":304,"meta":5,"style":5},"type CreateOrder struct {\n    repo OrderRepository\n    log  Logger\n}\n\nfunc (uc *CreateOrder) Execute(ctx context.Context, in CreateOrderInput) (CreateOrderOutput, error) {\n    \u002F\u002F ...\n    uc.log.InfoCtx(ctx, \"order created\", \"order_id\", o.ID(), \"user_id\", o.UserID())\n    return out, nil\n}\n",[20,5165,5166,5176,5183,5191,5195,5199,5241,5246,5277,5286],{"__ignoreMap":5},[308,5167,5168,5170,5172,5174],{"class":310,"line":311},[308,5169,398],{"class":321},[308,5171,1992],{"class":325},[308,5173,494],{"class":321},[308,5175,497],{"class":342},[308,5177,5178,5181],{"class":310,"line":318},[308,5179,5180],{"class":342},"    repo ",[308,5182,2004],{"class":325},[308,5184,5185,5188],{"class":310,"line":329},[308,5186,5187],{"class":342},"    log  ",[308,5189,5190],{"class":325},"Logger\n",[308,5192,5193],{"class":310,"line":336},[308,5194,533],{"class":342},[308,5196,5197],{"class":310,"line":346},[308,5198,333],{"emptyLinePlaceholder":332},[308,5200,5201,5203,5205,5207,5209,5211,5213,5215,5217,5219,5221,5223,5225,5227,5229,5231,5233,5235,5237,5239],{"class":310,"line":359},[308,5202,623],{"class":321},[308,5204,914],{"class":342},[308,5206,2092],{"class":632},[308,5208,666],{"class":321},[308,5210,101],{"class":325},[308,5212,924],{"class":342},[308,5214,2101],{"class":325},[308,5216,629],{"class":342},[308,5218,1653],{"class":632},[308,5220,1656],{"class":325},[308,5222,604],{"class":342},[308,5224,1661],{"class":325},[308,5226,80],{"class":342},[308,5228,2116],{"class":632},[308,5230,1888],{"class":325},[308,5232,663],{"class":342},[308,5234,2123],{"class":325},[308,5236,80],{"class":342},[308,5238,673],{"class":321},[308,5240,676],{"class":342},[308,5242,5243],{"class":310,"line":369},[308,5244,5245],{"class":314},"    \u002F\u002F ...\n",[308,5247,5248,5251,5254,5256,5259,5261,5263,5265,5267,5269,5271,5273,5275],{"class":310,"line":374},[308,5249,5250],{"class":342},"    uc.log.",[308,5252,5253],{"class":325},"InfoCtx",[308,5255,2429],{"class":342},[308,5257,5258],{"class":349},"\"order created\"",[308,5260,80],{"class":342},[308,5262,2357],{"class":349},[308,5264,3847],{"class":342},[308,5266,927],{"class":325},[308,5268,2365],{"class":342},[308,5270,2368],{"class":349},[308,5272,3847],{"class":342},[308,5274,960],{"class":325},[308,5276,2223],{"class":342},[308,5278,5279,5281,5284],{"class":310,"line":384},[308,5280,837],{"class":321},[308,5282,5283],{"class":342}," out, ",[308,5285,896],{"class":423},[308,5287,5288],{"class":310,"line":390},[308,5289,533],{"class":342},[16,5291,5292,5293,5295,5296,80,5299,80,5301,5304,5305,5308],{},"Тип ",[20,5294,165],{}," — интерфейс с минимальными методами (",[20,5297,5298],{},"Info",[20,5300,2978],{},[20,5302,5303],{},"WithFields","), не ",[20,5306,5307],{},"*zap.Logger"," напрямую. Это тот же DIP.",[33,5310,5312],{"id":5311},"метрики","Метрики",[16,5314,5315],{},"Метрики ставим там, где происходит измеряемое действие. Самое полезное:",[38,5317,5319],{"className":302,"code":5318,"language":304,"meta":5,"style":5},"\u002F\u002F usecase decorator с метриками\ntype CreateOrderMetrics struct {\n    inner usecase.CreateOrder\n    duration prometheus.Histogram\n    errors   prometheus.Counter\n}\n\nfunc (m *CreateOrderMetrics) Execute(ctx context.Context, in usecase.CreateOrderInput) (usecase.CreateOrderOutput, error) {\n    start := time.Now()\n    out, err := m.inner.Execute(ctx, in)\n    m.duration.Observe(time.Since(start).Seconds())\n    if err != nil {\n        m.errors.Inc()\n    }\n    return out, err\n}\n",[20,5320,5321,5326,5337,5348,5361,5373,5377,5381,5434,5447,5460,5482,5494,5504,5508,5515],{"__ignoreMap":5},[308,5322,5323],{"class":310,"line":311},[308,5324,5325],{"class":314},"\u002F\u002F usecase decorator с метриками\n",[308,5327,5328,5330,5333,5335],{"class":310,"line":318},[308,5329,398],{"class":321},[308,5331,5332],{"class":325}," CreateOrderMetrics",[308,5334,494],{"class":321},[308,5336,497],{"class":342},[308,5338,5339,5342,5344,5346],{"class":310,"line":329},[308,5340,5341],{"class":342},"    inner ",[308,5343,2597],{"class":325},[308,5345,604],{"class":342},[308,5347,2602],{"class":325},[308,5349,5350,5353,5356,5358],{"class":310,"line":336},[308,5351,5352],{"class":342},"    duration ",[308,5354,5355],{"class":325},"prometheus",[308,5357,604],{"class":342},[308,5359,5360],{"class":325},"Histogram\n",[308,5362,5363,5366,5368,5370],{"class":310,"line":346},[308,5364,5365],{"class":342},"    errors   ",[308,5367,5355],{"class":325},[308,5369,604],{"class":342},[308,5371,5372],{"class":325},"Counter\n",[308,5374,5375],{"class":310,"line":359},[308,5376,533],{"class":342},[308,5378,5379],{"class":310,"line":369},[308,5380,333],{"emptyLinePlaceholder":332},[308,5382,5383,5385,5387,5390,5392,5395,5397,5399,5401,5403,5405,5407,5409,5411,5413,5416,5418,5420,5422,5424,5426,5428,5430,5432],{"class":310,"line":374},[308,5384,623],{"class":321},[308,5386,914],{"class":342},[308,5388,5389],{"class":632},"m ",[308,5391,666],{"class":321},[308,5393,5394],{"class":325},"CreateOrderMetrics",[308,5396,924],{"class":342},[308,5398,2101],{"class":325},[308,5400,629],{"class":342},[308,5402,1653],{"class":632},[308,5404,1656],{"class":325},[308,5406,604],{"class":342},[308,5408,1661],{"class":325},[308,5410,80],{"class":342},[308,5412,2116],{"class":632},[308,5414,5415],{"class":325}," usecase",[308,5417,604],{"class":342},[308,5419,3087],{"class":325},[308,5421,663],{"class":342},[308,5423,2597],{"class":325},[308,5425,604],{"class":342},[308,5427,2123],{"class":325},[308,5429,80],{"class":342},[308,5431,673],{"class":321},[308,5433,676],{"class":342},[308,5435,5436,5439,5441,5443,5445],{"class":310,"line":384},[308,5437,5438],{"class":342},"    start ",[308,5440,767],{"class":321},[308,5442,4915],{"class":342},[308,5444,2220],{"class":325},[308,5446,4422],{"class":342},[308,5448,5449,5451,5453,5456,5458],{"class":310,"line":390},[308,5450,3060],{"class":342},[308,5452,767],{"class":321},[308,5454,5455],{"class":342}," m.inner.",[308,5457,2101],{"class":325},[308,5459,5119],{"class":342},[308,5461,5462,5465,5468,5471,5474,5477,5480],{"class":310,"line":395},[308,5463,5464],{"class":342},"    m.duration.",[308,5466,5467],{"class":325},"Observe",[308,5469,5470],{"class":342},"(time.",[308,5472,5473],{"class":325},"Since",[308,5475,5476],{"class":342},"(start).",[308,5478,5479],{"class":325},"Seconds",[308,5481,2223],{"class":342},[308,5483,5484,5486,5488,5490,5492],{"class":310,"line":407},[308,5485,682],{"class":321},[308,5487,2230],{"class":342},[308,5489,1154],{"class":321},[308,5491,702],{"class":423},[308,5493,497],{"class":342},[308,5495,5496,5499,5502],{"class":310,"line":412},[308,5497,5498],{"class":342},"        m.errors.",[308,5500,5501],{"class":325},"Inc",[308,5503,4422],{"class":342},[308,5505,5506],{"class":310,"line":420},[308,5507,711],{"class":342},[308,5509,5510,5512],{"class":310,"line":436},[308,5511,837],{"class":321},[308,5513,5514],{"class":342}," out, err\n",[308,5516,5517],{"class":310,"line":449},[308,5518,533],{"class":342},[16,5520,5521],{},"Domain — без метрик. Use case — с метриками длительности и ошибок. Adapter — с метриками внешних вызовов (DB, Kafka, HTTP-клиенты).",[33,5523,5525],{"id":5524},"транзакции-unitofwork","Транзакции (UnitOfWork)",[16,5527,5528],{},"Use case оркестрирует несколько операций, нужно атомарно. Делается через порт:",[38,5530,5532],{"className":302,"code":5531,"language":304,"meta":5,"style":5},"\u002F\u002F internal\u002Fusecase\u002Fport.go\ntype TxManager interface {\n    Do(ctx context.Context, fn func(ctx context.Context) error) error\n}\n",[20,5533,5534,5538,5549,5589],{"__ignoreMap":5},[308,5535,5536],{"class":310,"line":311},[308,5537,1577],{"class":314},[308,5539,5540,5542,5545,5547],{"class":310,"line":318},[308,5541,398],{"class":321},[308,5543,5544],{"class":325}," TxManager",[308,5546,120],{"class":321},[308,5548,497],{"class":342},[308,5550,5551,5554,5556,5558,5560,5562,5564,5566,5569,5571,5573,5575,5577,5579,5581,5583,5585,5587],{"class":310,"line":329},[308,5552,5553],{"class":325},"    Do",[308,5555,629],{"class":342},[308,5557,1653],{"class":632},[308,5559,1656],{"class":325},[308,5561,604],{"class":342},[308,5563,1661],{"class":325},[308,5565,80],{"class":342},[308,5567,5568],{"class":632},"fn",[308,5570,4748],{"class":321},[308,5572,629],{"class":342},[308,5574,1653],{"class":632},[308,5576,1656],{"class":325},[308,5578,604],{"class":342},[308,5580,1661],{"class":325},[308,5582,924],{"class":342},[308,5584,673],{"class":321},[308,5586,924],{"class":342},[308,5588,1681],{"class":321},[308,5590,5591],{"class":310,"line":336},[308,5592,533],{"class":342},[38,5594,5596],{"className":302,"code":5595,"language":304,"meta":5,"style":5},"\u002F\u002F internal\u002Fadapter\u002Fpostgres\u002Ftx_manager.go\ntype TxManager struct{ pool *pgxpool.Pool }\n\ntype txKey struct{}\n\nfunc (m *TxManager) Do(ctx context.Context, fn func(ctx context.Context) error) error {\n    tx, err := m.pool.Begin(ctx)\n    if err != nil {\n        return err\n    }\n    if err := fn(context.WithValue(ctx, txKey{}, tx)); err != nil {\n        _ = tx.Rollback(ctx)\n        return err\n    }\n    return tx.Commit(ctx)\n}\n\n\u002F\u002F executor — выбирает Tx из контекста или pool\nfunc executor(ctx context.Context, pool *pgxpool.Pool) interface {\n    Exec(context.Context, string, ...any) (pgconn.CommandTag, error)\n    QueryRow(context.Context, string, ...any) pgx.Row\n} {\n    if tx, ok := ctx.Value(txKey{}).(pgx.Tx); ok {\n        return tx\n    }\n    return pool\n}\n",[20,5597,5598,5603,5625,5629,5640,5644,5698,5714,5726,5732,5736,5766,5781,5787,5791,5802,5806,5810,5815,5850,5889,5922,5927,5959,5966,5970,5977],{"__ignoreMap":5},[308,5599,5600],{"class":310,"line":311},[308,5601,5602],{"class":314},"\u002F\u002F internal\u002Fadapter\u002Fpostgres\u002Ftx_manager.go\n",[308,5604,5605,5607,5609,5611,5614,5616,5618,5620,5622],{"class":310,"line":318},[308,5606,398],{"class":321},[308,5608,5544],{"class":325},[308,5610,494],{"class":321},[308,5612,5613],{"class":342},"{ pool ",[308,5615,666],{"class":321},[308,5617,3614],{"class":325},[308,5619,604],{"class":342},[308,5621,3648],{"class":325},[308,5623,5624],{"class":342}," }\n",[308,5626,5627],{"class":310,"line":329},[308,5628,333],{"emptyLinePlaceholder":332},[308,5630,5631,5633,5636,5638],{"class":310,"line":336},[308,5632,398],{"class":321},[308,5634,5635],{"class":325}," txKey",[308,5637,494],{"class":321},[308,5639,4888],{"class":342},[308,5641,5642],{"class":310,"line":346},[308,5643,333],{"emptyLinePlaceholder":332},[308,5645,5646,5648,5650,5652,5654,5657,5659,5662,5664,5666,5668,5670,5672,5674,5676,5678,5680,5682,5684,5686,5688,5690,5692,5694,5696],{"class":310,"line":359},[308,5647,623],{"class":321},[308,5649,914],{"class":342},[308,5651,5389],{"class":632},[308,5653,666],{"class":321},[308,5655,5656],{"class":325},"TxManager",[308,5658,924],{"class":342},[308,5660,5661],{"class":325},"Do",[308,5663,629],{"class":342},[308,5665,1653],{"class":632},[308,5667,1656],{"class":325},[308,5669,604],{"class":342},[308,5671,1661],{"class":325},[308,5673,80],{"class":342},[308,5675,5568],{"class":632},[308,5677,4748],{"class":321},[308,5679,629],{"class":342},[308,5681,1653],{"class":632},[308,5683,1656],{"class":325},[308,5685,604],{"class":342},[308,5687,1661],{"class":325},[308,5689,924],{"class":342},[308,5691,673],{"class":321},[308,5693,924],{"class":342},[308,5695,673],{"class":321},[308,5697,497],{"class":342},[308,5699,5700,5703,5705,5708,5711],{"class":310,"line":369},[308,5701,5702],{"class":342},"    tx, err ",[308,5704,767],{"class":321},[308,5706,5707],{"class":342}," m.pool.",[308,5709,5710],{"class":325},"Begin",[308,5712,5713],{"class":342},"(ctx)\n",[308,5715,5716,5718,5720,5722,5724],{"class":310,"line":374},[308,5717,682],{"class":321},[308,5719,2230],{"class":342},[308,5721,1154],{"class":321},[308,5723,702],{"class":423},[308,5725,497],{"class":342},[308,5727,5728,5730],{"class":310,"line":384},[308,5729,699],{"class":321},[308,5731,3880],{"class":342},[308,5733,5734],{"class":310,"line":390},[308,5735,711],{"class":342},[308,5737,5738,5740,5742,5744,5747,5749,5752,5754,5757,5760,5762,5764],{"class":310,"line":395},[308,5739,682],{"class":321},[308,5741,2230],{"class":342},[308,5743,767],{"class":321},[308,5745,5746],{"class":325}," fn",[308,5748,4457],{"class":342},[308,5750,5751],{"class":325},"WithValue",[308,5753,2429],{"class":342},[308,5755,5756],{"class":325},"txKey",[308,5758,5759],{"class":342},"{}, tx)); err ",[308,5761,1154],{"class":321},[308,5763,702],{"class":423},[308,5765,497],{"class":342},[308,5767,5768,5771,5773,5776,5779],{"class":310,"line":407},[308,5769,5770],{"class":342},"        _ ",[308,5772,1179],{"class":321},[308,5774,5775],{"class":342}," tx.",[308,5777,5778],{"class":325},"Rollback",[308,5780,5713],{"class":342},[308,5782,5783,5785],{"class":310,"line":412},[308,5784,699],{"class":321},[308,5786,3880],{"class":342},[308,5788,5789],{"class":310,"line":420},[308,5790,711],{"class":342},[308,5792,5793,5795,5797,5800],{"class":310,"line":436},[308,5794,837],{"class":321},[308,5796,5775],{"class":342},[308,5798,5799],{"class":325},"Commit",[308,5801,5713],{"class":342},[308,5803,5804],{"class":310,"line":449},[308,5805,533],{"class":342},[308,5807,5808],{"class":310,"line":463},[308,5809,333],{"emptyLinePlaceholder":332},[308,5811,5812],{"class":310,"line":476},[308,5813,5814],{"class":314},"\u002F\u002F executor — выбирает Tx из контекста или pool\n",[308,5816,5817,5819,5821,5823,5825,5827,5829,5831,5833,5835,5837,5839,5841,5843,5845,5848],{"class":310,"line":481},[308,5818,623],{"class":321},[308,5820,3794],{"class":325},[308,5822,629],{"class":342},[308,5824,1653],{"class":632},[308,5826,1656],{"class":325},[308,5828,604],{"class":342},[308,5830,1661],{"class":325},[308,5832,80],{"class":342},[308,5834,3639],{"class":632},[308,5836,1669],{"class":321},[308,5838,3614],{"class":325},[308,5840,604],{"class":342},[308,5842,3648],{"class":325},[308,5844,924],{"class":342},[308,5846,5847],{"class":321},"interface",[308,5849,497],{"class":342},[308,5851,5852,5855,5857,5859,5861,5863,5865,5867,5869,5871,5873,5875,5878,5880,5883,5885,5887],{"class":310,"line":486},[308,5853,5854],{"class":325},"    Exec",[308,5856,629],{"class":342},[308,5858,1601],{"class":325},[308,5860,604],{"class":342},[308,5862,1661],{"class":325},[308,5864,80],{"class":342},[308,5866,933],{"class":321},[308,5868,80],{"class":342},[308,5870,1019],{"class":321},[308,5872,2351],{"class":325},[308,5874,663],{"class":342},[308,5876,5877],{"class":325},"pgconn",[308,5879,604],{"class":342},[308,5881,5882],{"class":325},"CommandTag",[308,5884,80],{"class":342},[308,5886,673],{"class":321},[308,5888,387],{"class":342},[308,5890,5891,5894,5896,5898,5900,5902,5904,5906,5908,5910,5912,5914,5917,5919],{"class":310,"line":500},[308,5892,5893],{"class":325},"    QueryRow",[308,5895,629],{"class":342},[308,5897,1601],{"class":325},[308,5899,604],{"class":342},[308,5901,1661],{"class":325},[308,5903,80],{"class":342},[308,5905,933],{"class":321},[308,5907,80],{"class":342},[308,5909,1019],{"class":321},[308,5911,2351],{"class":325},[308,5913,924],{"class":342},[308,5915,5916],{"class":325},"pgx",[308,5918,604],{"class":342},[308,5920,5921],{"class":325},"Row\n",[308,5923,5924],{"class":310,"line":509},[308,5925,5926],{"class":342},"} {\n",[308,5928,5929,5931,5934,5936,5939,5942,5944,5946,5949,5951,5953,5956],{"class":310,"line":518},[308,5930,682],{"class":321},[308,5932,5933],{"class":342}," tx, ok ",[308,5935,767],{"class":321},[308,5937,5938],{"class":342}," ctx.",[308,5940,5941],{"class":325},"Value",[308,5943,629],{"class":342},[308,5945,5756],{"class":325},[308,5947,5948],{"class":342},"{}).(",[308,5950,5916],{"class":325},[308,5952,604],{"class":342},[308,5954,5955],{"class":325},"Tx",[308,5957,5958],{"class":342},"); ok {\n",[308,5960,5961,5963],{"class":310,"line":530},[308,5962,699],{"class":321},[308,5964,5965],{"class":342}," tx\n",[308,5967,5968],{"class":310,"line":536},[308,5969,711],{"class":342},[308,5971,5972,5974],{"class":310,"line":541},[308,5973,837],{"class":321},[308,5975,5976],{"class":342}," pool\n",[308,5978,5979],{"class":310,"line":553},[308,5980,533],{"class":342},[16,5982,5983],{},"Use case использует:",[38,5985,5987],{"className":302,"code":5986,"language":304,"meta":5,"style":5},"func (uc *CancelOrder) Execute(ctx context.Context, id string) error {\n    return uc.tx.Do(ctx, func(ctx context.Context) error {\n        o, err := uc.repo.FindByID(ctx, id)\n        if err != nil {\n            return err\n        }\n        if err := o.Cancel(); err != nil {\n            return err\n        }\n        if err := uc.repo.Save(ctx, o); err != nil {\n            return err\n        }\n        return uc.refundsRepo.Schedule(ctx, o.ID())\n    })\n}\n",[20,5988,5989,6027,6056,6070,6082,6088,6092,6114,6120,6124,6144,6150,6154,6171,6175],{"__ignoreMap":5},[308,5990,5991,5993,5995,5997,5999,6001,6003,6005,6007,6009,6011,6013,6015,6017,6019,6021,6023,6025],{"class":310,"line":311},[308,5992,623],{"class":321},[308,5994,914],{"class":342},[308,5996,2092],{"class":632},[308,5998,666],{"class":321},[308,6000,104],{"class":325},[308,6002,924],{"class":342},[308,6004,2101],{"class":325},[308,6006,629],{"class":342},[308,6008,1653],{"class":632},[308,6010,1656],{"class":325},[308,6012,604],{"class":342},[308,6014,1661],{"class":325},[308,6016,80],{"class":342},[308,6018,1320],{"class":632},[308,6020,636],{"class":321},[308,6022,924],{"class":342},[308,6024,673],{"class":321},[308,6026,497],{"class":342},[308,6028,6029,6031,6034,6036,6038,6040,6042,6044,6046,6048,6050,6052,6054],{"class":310,"line":318},[308,6030,837],{"class":321},[308,6032,6033],{"class":342}," uc.tx.",[308,6035,5661],{"class":325},[308,6037,2429],{"class":342},[308,6039,623],{"class":321},[308,6041,629],{"class":342},[308,6043,1653],{"class":632},[308,6045,1656],{"class":325},[308,6047,604],{"class":342},[308,6049,1661],{"class":325},[308,6051,924],{"class":342},[308,6053,673],{"class":321},[308,6055,497],{"class":342},[308,6057,6058,6061,6063,6065,6067],{"class":310,"line":329},[308,6059,6060],{"class":342},"        o, err ",[308,6062,767],{"class":321},[308,6064,2281],{"class":342},[308,6066,3905],{"class":325},[308,6068,6069],{"class":342},"(ctx, id)\n",[308,6071,6072,6074,6076,6078,6080],{"class":310,"line":336},[308,6073,779],{"class":321},[308,6075,2230],{"class":342},[308,6077,1154],{"class":321},[308,6079,702],{"class":423},[308,6081,497],{"class":342},[308,6083,6084,6086],{"class":310,"line":346},[308,6085,795],{"class":321},[308,6087,3880],{"class":342},[308,6089,6090],{"class":310,"line":359},[308,6091,806],{"class":342},[308,6093,6094,6096,6098,6100,6103,6105,6108,6110,6112],{"class":310,"line":369},[308,6095,779],{"class":321},[308,6097,2230],{"class":342},[308,6099,767],{"class":321},[308,6101,6102],{"class":342}," o.",[308,6104,1218],{"class":325},[308,6106,6107],{"class":342},"(); err ",[308,6109,1154],{"class":321},[308,6111,702],{"class":423},[308,6113,497],{"class":342},[308,6115,6116,6118],{"class":310,"line":374},[308,6117,795],{"class":321},[308,6119,3880],{"class":342},[308,6121,6122],{"class":310,"line":384},[308,6123,806],{"class":342},[308,6125,6126,6128,6130,6132,6134,6136,6138,6140,6142],{"class":310,"line":390},[308,6127,779],{"class":321},[308,6129,2230],{"class":342},[308,6131,767],{"class":321},[308,6133,2281],{"class":342},[308,6135,2284],{"class":325},[308,6137,2287],{"class":342},[308,6139,1154],{"class":321},[308,6141,702],{"class":423},[308,6143,497],{"class":342},[308,6145,6146,6148],{"class":310,"line":395},[308,6147,795],{"class":321},[308,6149,3880],{"class":342},[308,6151,6152],{"class":310,"line":407},[308,6153,806],{"class":342},[308,6155,6156,6158,6161,6164,6167,6169],{"class":310,"line":412},[308,6157,699],{"class":321},[308,6159,6160],{"class":342}," uc.refundsRepo.",[308,6162,6163],{"class":325},"Schedule",[308,6165,6166],{"class":342},"(ctx, o.",[308,6168,927],{"class":325},[308,6170,2223],{"class":342},[308,6172,6173],{"class":310,"line":420},[308,6174,3104],{"class":342},[308,6176,6177],{"class":310,"line":436},[308,6178,533],{"class":342},[16,6180,6181],{},"Use case ничего не знает про SQL-транзакцию — только про абстракцию «сделай это атомарно».",[16,6183,6184,6185,6188,6189,6192],{},"Важная оговорка: ",[20,6186,6187],{},"context.WithValue"," для ",[20,6190,6191],{},"tx"," — компромисс, а не универсальная рекомендация. Он удобен, когда хочется сохранить сигнатуры репозиториев, но может превратить context в dependency bag. В более строгом варианте transaction runner явно передаёт объект repositories:",[38,6194,6196],{"className":302,"code":6195,"language":304,"meta":5,"style":5},"type Repositories interface {\n    Orders() OrderRepository\n    Refunds() RefundRepository\n}\n\ntype TxRunner interface {\n    InTx(ctx context.Context, fn func(ctx context.Context, repos Repositories) error) error\n}\n",[20,6197,6198,6209,6218,6228,6232,6236,6247,6293],{"__ignoreMap":5},[308,6199,6200,6202,6205,6207],{"class":310,"line":311},[308,6201,398],{"class":321},[308,6203,6204],{"class":325}," Repositories",[308,6206,120],{"class":321},[308,6208,497],{"class":342},[308,6210,6211,6214,6216],{"class":310,"line":318},[308,6212,6213],{"class":325},"    Orders",[308,6215,930],{"class":342},[308,6217,2004],{"class":325},[308,6219,6220,6223,6225],{"class":310,"line":329},[308,6221,6222],{"class":325},"    Refunds",[308,6224,930],{"class":342},[308,6226,6227],{"class":325},"RefundRepository\n",[308,6229,6230],{"class":310,"line":336},[308,6231,533],{"class":342},[308,6233,6234],{"class":310,"line":346},[308,6235,333],{"emptyLinePlaceholder":332},[308,6237,6238,6240,6243,6245],{"class":310,"line":359},[308,6239,398],{"class":321},[308,6241,6242],{"class":325}," TxRunner",[308,6244,120],{"class":321},[308,6246,497],{"class":342},[308,6248,6249,6252,6254,6256,6258,6260,6262,6264,6266,6268,6270,6272,6274,6276,6278,6280,6283,6285,6287,6289,6291],{"class":310,"line":369},[308,6250,6251],{"class":325},"    InTx",[308,6253,629],{"class":342},[308,6255,1653],{"class":632},[308,6257,1656],{"class":325},[308,6259,604],{"class":342},[308,6261,1661],{"class":325},[308,6263,80],{"class":342},[308,6265,5568],{"class":632},[308,6267,4748],{"class":321},[308,6269,629],{"class":342},[308,6271,1653],{"class":632},[308,6273,1656],{"class":325},[308,6275,604],{"class":342},[308,6277,1661],{"class":325},[308,6279,80],{"class":342},[308,6281,6282],{"class":632},"repos",[308,6284,6204],{"class":325},[308,6286,924],{"class":342},[308,6288,673],{"class":321},[308,6290,924],{"class":342},[308,6292,1681],{"class":321},[308,6294,6295],{"class":310,"line":374},[308,6296,533],{"class":342},[16,6298,6299,6300,6303],{},"Так use case всё ещё не знает про ",[20,6301,6302],{},"pgx.Tx",", но зависимость от транзакционного набора репозиториев становится видимой в типах.",[33,6305,6307],{"id":6306},"тесты","Тесты",[33,6309,6311],{"id":6310},"unit-test-на-use-case-мок-репозиторий","Unit-test на use case (мок-репозиторий)",[38,6313,6315],{"className":302,"code":6314,"language":304,"meta":5,"style":5},"\u002F\u002F internal\u002Fusecase\u002Fcreate_order_test.go\npackage usecase_test\n\nimport (\n    \"context\"\n    \"testing\"\n    \"time\"\n\n    \"github.com\u002Fstretchr\u002Ftestify\u002Frequire\"\n\n    \"myproject\u002Finternal\u002Fdomain\u002Forder\"\n    \"myproject\u002Finternal\u002Fusecase\"\n)\n\ntype fakeRepo struct{ saved []*order.Order }\nfunc (f *fakeRepo) Save(_ context.Context, o *order.Order) error  { f.saved = append(f.saved, o); return nil }\nfunc (f *fakeRepo) FindByID(_ context.Context, _ string) (*order.Order, error) { return nil, nil }\n\ntype fakePub struct{ events int }\nfunc (f *fakePub) Publish(_ context.Context, _ string, _ []byte) error { f.events++; return nil }\n\ntype fixedClock struct{ t time.Time }\nfunc (c fixedClock) Now() time.Time { return c.t }\n\nfunc TestCreateOrder_OK(t *testing.T) {\n    repo := &fakeRepo{}\n    pub  := &fakePub{}\n    uc := usecase.NewCreateOrder(repo, pub, fixedClock{t: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)})\n\n    out, err := uc.Execute(context.Background(), usecase.CreateOrderInput{\n        UserID: \"u-1\",\n        Items:  []usecase.CreateOrderItem{{SKU: \"A\", Quantity: 2, Price: 1000}},\n    })\n\n    require.NoError(t, err)\n    require.NotEmpty(t, out.OrderID)\n    require.Equal(t, int64(2000), out.Total)\n    require.Len(t, repo.saved, 1)\n    require.Equal(t, 1, pub.events)\n}\n\nfunc TestCreateOrder_EmptyItems(t *testing.T) {\n    uc := usecase.NewCreateOrder(&fakeRepo{}, &fakePub{}, fixedClock{})\n    _, err := uc.Execute(context.Background(), usecase.CreateOrderInput{UserID: \"u-1\"})\n    require.ErrorIs(t, err, order.ErrEmptyItems)\n}\n",[20,6316,6317,6322,6329,6333,6339,6347,6356,6364,6368,6377,6381,6389,6397,6401,6405,6427,6488,6547,6551,6567,6627,6631,6651,6682,6686,6709,6721,6734,6790,6794,6819,6830,6862,6866,6870,6881,6891,6911,6925,6938,6942,6946,6967,6996,7025,7035],{"__ignoreMap":5},[308,6318,6319],{"class":310,"line":311},[308,6320,6321],{"class":314},"\u002F\u002F internal\u002Fusecase\u002Fcreate_order_test.go\n",[308,6323,6324,6326],{"class":310,"line":318},[308,6325,322],{"class":321},[308,6327,6328],{"class":325}," usecase_test\n",[308,6330,6331],{"class":310,"line":329},[308,6332,333],{"emptyLinePlaceholder":332},[308,6334,6335,6337],{"class":310,"line":336},[308,6336,339],{"class":321},[308,6338,343],{"class":342},[308,6340,6341,6343,6345],{"class":310,"line":346},[308,6342,350],{"class":349},[308,6344,1601],{"class":325},[308,6346,356],{"class":349},[308,6348,6349,6351,6354],{"class":310,"line":359},[308,6350,350],{"class":349},[308,6352,6353],{"class":325},"testing",[308,6355,356],{"class":349},[308,6357,6358,6360,6362],{"class":310,"line":369},[308,6359,350],{"class":349},[308,6361,364],{"class":325},[308,6363,356],{"class":349},[308,6365,6366],{"class":310,"line":374},[308,6367,333],{"emptyLinePlaceholder":332},[308,6369,6370,6372,6375],{"class":310,"line":384},[308,6371,350],{"class":349},[308,6373,6374],{"class":325},"github.com\u002Fstretchr\u002Ftestify\u002Frequire",[308,6376,356],{"class":349},[308,6378,6379],{"class":310,"line":390},[308,6380,333],{"emptyLinePlaceholder":332},[308,6382,6383,6385,6387],{"class":310,"line":395},[308,6384,350],{"class":349},[308,6386,1622],{"class":325},[308,6388,356],{"class":349},[308,6390,6391,6393,6395],{"class":310,"line":407},[308,6392,350],{"class":349},[308,6394,2566],{"class":325},[308,6396,356],{"class":349},[308,6398,6399],{"class":310,"line":412},[308,6400,387],{"class":342},[308,6402,6403],{"class":310,"line":420},[308,6404,333],{"emptyLinePlaceholder":332},[308,6406,6407,6409,6412,6414,6417,6419,6421,6423,6425],{"class":310,"line":436},[308,6408,398],{"class":321},[308,6410,6411],{"class":325}," fakeRepo",[308,6413,494],{"class":321},[308,6415,6416],{"class":342},"{ saved []",[308,6418,666],{"class":321},[308,6420,1672],{"class":325},[308,6422,604],{"class":342},[308,6424,79],{"class":325},[308,6426,5624],{"class":342},[308,6428,6429,6431,6433,6436,6438,6441,6443,6445,6447,6450,6452,6454,6456,6458,6460,6462,6464,6466,6468,6470,6472,6475,6477,6479,6482,6484,6486],{"class":310,"line":449},[308,6430,623],{"class":321},[308,6432,914],{"class":342},[308,6434,6435],{"class":632},"f ",[308,6437,666],{"class":321},[308,6439,6440],{"class":325},"fakeRepo",[308,6442,924],{"class":342},[308,6444,2284],{"class":325},[308,6446,629],{"class":342},[308,6448,6449],{"class":632},"_",[308,6451,1656],{"class":325},[308,6453,604],{"class":342},[308,6455,1661],{"class":325},[308,6457,80],{"class":342},[308,6459,1666],{"class":632},[308,6461,1669],{"class":321},[308,6463,1672],{"class":325},[308,6465,604],{"class":342},[308,6467,79],{"class":325},[308,6469,924],{"class":342},[308,6471,673],{"class":321},[308,6473,6474],{"class":342},"  { f.saved ",[308,6476,1179],{"class":321},[308,6478,1003],{"class":325},[308,6480,6481],{"class":342},"(f.saved, o); ",[308,6483,939],{"class":321},[308,6485,702],{"class":423},[308,6487,5624],{"class":342},[308,6489,6490,6492,6494,6496,6498,6500,6502,6504,6506,6508,6510,6512,6514,6516,6518,6520,6522,6524,6526,6528,6530,6532,6534,6537,6539,6541,6543,6545],{"class":310,"line":463},[308,6491,623],{"class":321},[308,6493,914],{"class":342},[308,6495,6435],{"class":632},[308,6497,666],{"class":321},[308,6499,6440],{"class":325},[308,6501,924],{"class":342},[308,6503,3905],{"class":325},[308,6505,629],{"class":342},[308,6507,6449],{"class":632},[308,6509,1656],{"class":325},[308,6511,604],{"class":342},[308,6513,1661],{"class":325},[308,6515,80],{"class":342},[308,6517,6449],{"class":632},[308,6519,636],{"class":321},[308,6521,663],{"class":342},[308,6523,666],{"class":321},[308,6525,1672],{"class":325},[308,6527,604],{"class":342},[308,6529,79],{"class":325},[308,6531,80],{"class":342},[308,6533,673],{"class":321},[308,6535,6536],{"class":342},") { ",[308,6538,939],{"class":321},[308,6540,702],{"class":423},[308,6542,80],{"class":342},[308,6544,1013],{"class":423},[308,6546,5624],{"class":342},[308,6548,6549],{"class":310,"line":476},[308,6550,333],{"emptyLinePlaceholder":332},[308,6552,6553,6555,6558,6560,6563,6565],{"class":310,"line":481},[308,6554,398],{"class":321},[308,6556,6557],{"class":325}," fakePub",[308,6559,494],{"class":321},[308,6561,6562],{"class":342},"{ events ",[308,6564,2777],{"class":321},[308,6566,5624],{"class":342},[308,6568,6569,6571,6573,6575,6577,6580,6582,6584,6586,6588,6590,6592,6594,6596,6598,6600,6602,6604,6606,6608,6610,6612,6615,6618,6621,6623,6625],{"class":310,"line":486},[308,6570,623],{"class":321},[308,6572,914],{"class":342},[308,6574,6435],{"class":632},[308,6576,666],{"class":321},[308,6578,6579],{"class":325},"fakePub",[308,6581,924],{"class":342},[308,6583,2426],{"class":325},[308,6585,629],{"class":342},[308,6587,6449],{"class":632},[308,6589,1656],{"class":325},[308,6591,604],{"class":342},[308,6593,1661],{"class":325},[308,6595,80],{"class":342},[308,6597,6449],{"class":632},[308,6599,636],{"class":321},[308,6601,80],{"class":342},[308,6603,6449],{"class":632},[308,6605,644],{"class":342},[308,6607,1801],{"class":321},[308,6609,924],{"class":342},[308,6611,673],{"class":321},[308,6613,6614],{"class":342}," { f.events",[308,6616,6617],{"class":321},"++",[308,6619,6620],{"class":342},"; ",[308,6622,939],{"class":321},[308,6624,702],{"class":423},[308,6626,5624],{"class":342},[308,6628,6629],{"class":310,"line":500},[308,6630,333],{"emptyLinePlaceholder":332},[308,6632,6633,6635,6638,6640,6643,6645,6647,6649],{"class":310,"line":509},[308,6634,398],{"class":321},[308,6636,6637],{"class":325}," fixedClock",[308,6639,494],{"class":321},[308,6641,6642],{"class":342},"{ t ",[308,6644,364],{"class":325},[308,6646,604],{"class":342},[308,6648,660],{"class":325},[308,6650,5624],{"class":342},[308,6652,6653,6655,6657,6660,6663,6665,6667,6669,6671,6673,6675,6677,6679],{"class":310,"line":518},[308,6654,623],{"class":321},[308,6656,914],{"class":342},[308,6658,6659],{"class":632},"c ",[308,6661,6662],{"class":325},"fixedClock",[308,6664,924],{"class":342},[308,6666,2220],{"class":325},[308,6668,930],{"class":342},[308,6670,364],{"class":325},[308,6672,604],{"class":342},[308,6674,660],{"class":325},[308,6676,1109],{"class":342},[308,6678,939],{"class":321},[308,6680,6681],{"class":342}," c.t }\n",[308,6683,6684],{"class":310,"line":530},[308,6685,333],{"emptyLinePlaceholder":332},[308,6687,6688,6690,6693,6695,6698,6700,6702,6704,6707],{"class":310,"line":536},[308,6689,623],{"class":321},[308,6691,6692],{"class":325}," TestCreateOrder_OK",[308,6694,629],{"class":342},[308,6696,6697],{"class":632},"t",[308,6699,1669],{"class":321},[308,6701,6353],{"class":325},[308,6703,604],{"class":342},[308,6705,6706],{"class":325},"T",[308,6708,676],{"class":342},[308,6710,6711,6713,6715,6717,6719],{"class":310,"line":541},[308,6712,5180],{"class":342},[308,6714,767],{"class":321},[308,6716,840],{"class":321},[308,6718,6440],{"class":325},[308,6720,4888],{"class":342},[308,6722,6723,6726,6728,6730,6732],{"class":310,"line":553},[308,6724,6725],{"class":342},"    pub  ",[308,6727,767],{"class":321},[308,6729,840],{"class":321},[308,6731,6579],{"class":325},[308,6733,4888],{"class":342},[308,6735,6736,6739,6741,6743,6745,6748,6750,6753,6756,6758,6761,6763,6766,6768,6770,6772,6775,6777,6779,6781,6783,6785,6787],{"class":310,"line":561},[308,6737,6738],{"class":342},"    uc ",[308,6740,767],{"class":321},[308,6742,4613],{"class":342},[308,6744,4616],{"class":325},[308,6746,6747],{"class":342},"(repo, pub, ",[308,6749,6662],{"class":325},[308,6751,6752],{"class":342},"{t: time.",[308,6754,6755],{"class":325},"Date",[308,6757,629],{"class":342},[308,6759,6760],{"class":423},"2025",[308,6762,80],{"class":342},[308,6764,6765],{"class":423},"1",[308,6767,80],{"class":342},[308,6769,6765],{"class":423},[308,6771,80],{"class":342},[308,6773,6774],{"class":423},"12",[308,6776,80],{"class":342},[308,6778,2152],{"class":423},[308,6780,80],{"class":342},[308,6782,2152],{"class":423},[308,6784,80],{"class":342},[308,6786,2152],{"class":423},[308,6788,6789],{"class":342},", time.UTC)})\n",[308,6791,6792],{"class":310,"line":569},[308,6793,333],{"emptyLinePlaceholder":332},[308,6795,6796,6798,6800,6803,6805,6807,6809,6811,6813,6815,6817],{"class":310,"line":578},[308,6797,3060],{"class":342},[308,6799,767],{"class":321},[308,6801,6802],{"class":342}," uc.",[308,6804,2101],{"class":325},[308,6806,4457],{"class":342},[308,6808,4460],{"class":325},[308,6810,2365],{"class":342},[308,6812,2597],{"class":325},[308,6814,604],{"class":342},[308,6816,3087],{"class":325},[308,6818,845],{"class":342},[308,6820,6821,6824,6827],{"class":310,"line":587},[308,6822,6823],{"class":342},"        UserID: ",[308,6825,6826],{"class":349},"\"u-1\"",[308,6828,6829],{"class":342},",\n",[308,6831,6832,6835,6837,6839,6841,6844,6847,6850,6853,6856,6859],{"class":310,"line":596},[308,6833,6834],{"class":342},"        Items:  []",[308,6836,2597],{"class":325},[308,6838,604],{"class":342},[308,6840,3005],{"class":325},[308,6842,6843],{"class":342},"{{SKU: ",[308,6845,6846],{"class":349},"\"A\"",[308,6848,6849],{"class":342},", Quantity: ",[308,6851,6852],{"class":423},"2",[308,6854,6855],{"class":342},", Price: ",[308,6857,6858],{"class":423},"1000",[308,6860,6861],{"class":342},"}},\n",[308,6863,6864],{"class":310,"line":610},[308,6865,3104],{"class":342},[308,6867,6868],{"class":310,"line":615},[308,6869,333],{"emptyLinePlaceholder":332},[308,6871,6872,6875,6878],{"class":310,"line":620},[308,6873,6874],{"class":342},"    require.",[308,6876,6877],{"class":325},"NoError",[308,6879,6880],{"class":342},"(t, err)\n",[308,6882,6883,6885,6888],{"class":310,"line":679},[308,6884,6874],{"class":342},[308,6886,6887],{"class":325},"NotEmpty",[308,6889,6890],{"class":342},"(t, out.OrderID)\n",[308,6892,6893,6895,6898,6901,6903,6905,6908],{"class":310,"line":696},[308,6894,6874],{"class":342},[308,6896,6897],{"class":325},"Equal",[308,6899,6900],{"class":342},"(t, ",[308,6902,524],{"class":321},[308,6904,629],{"class":342},[308,6906,6907],{"class":423},"2000",[308,6909,6910],{"class":342},"), out.Total)\n",[308,6912,6913,6915,6918,6921,6923],{"class":310,"line":708},[308,6914,6874],{"class":342},[308,6916,6917],{"class":325},"Len",[308,6919,6920],{"class":342},"(t, repo.saved, ",[308,6922,6765],{"class":423},[308,6924,387],{"class":342},[308,6926,6927,6929,6931,6933,6935],{"class":310,"line":714},[308,6928,6874],{"class":342},[308,6930,6897],{"class":325},[308,6932,6900],{"class":342},[308,6934,6765],{"class":423},[308,6936,6937],{"class":342},", pub.events)\n",[308,6939,6940],{"class":310,"line":732},[308,6941,533],{"class":342},[308,6943,6944],{"class":310,"line":742},[308,6945,333],{"emptyLinePlaceholder":332},[308,6947,6948,6950,6953,6955,6957,6959,6961,6963,6965],{"class":310,"line":747},[308,6949,623],{"class":321},[308,6951,6952],{"class":325}," TestCreateOrder_EmptyItems",[308,6954,629],{"class":342},[308,6956,6697],{"class":632},[308,6958,1669],{"class":321},[308,6960,6353],{"class":325},[308,6962,604],{"class":342},[308,6964,6706],{"class":325},[308,6966,676],{"class":342},[308,6968,6969,6971,6973,6975,6977,6979,6981,6983,6986,6988,6990,6992,6994],{"class":310,"line":758},[308,6970,6738],{"class":342},[308,6972,767],{"class":321},[308,6974,4613],{"class":342},[308,6976,4616],{"class":325},[308,6978,629],{"class":342},[308,6980,2898],{"class":321},[308,6982,6440],{"class":325},[308,6984,6985],{"class":342},"{}, ",[308,6987,2898],{"class":321},[308,6989,6579],{"class":325},[308,6991,6985],{"class":342},[308,6993,6662],{"class":325},[308,6995,4625],{"class":342},[308,6997,6998,7000,7002,7004,7006,7008,7010,7012,7014,7016,7018,7021,7023],{"class":310,"line":776},[308,6999,3806],{"class":342},[308,7001,767],{"class":321},[308,7003,6802],{"class":342},[308,7005,2101],{"class":325},[308,7007,4457],{"class":342},[308,7009,4460],{"class":325},[308,7011,2365],{"class":342},[308,7013,2597],{"class":325},[308,7015,604],{"class":342},[308,7017,3087],{"class":325},[308,7019,7020],{"class":342},"{UserID: ",[308,7022,6826],{"class":349},[308,7024,2931],{"class":342},[308,7026,7027,7029,7032],{"class":310,"line":792},[308,7028,6874],{"class":342},[308,7030,7031],{"class":325},"ErrorIs",[308,7033,7034],{"class":342},"(t, err, order.ErrEmptyItems)\n",[308,7036,7037],{"class":310,"line":803},[308,7038,533],{"class":342},[16,7040,7041,7042,7045],{},"Без поднятой БД, без httptest, без ",[20,7043,7044],{},"pgxmock",". Запускается за миллисекунды.",[33,7047,7049],{"id":7048},"integration-test-на-handler-с-реальной-бд-через-testcontainers","Integration-test на handler (с реальной БД через testcontainers)",[38,7051,7053],{"className":302,"code":7052,"language":304,"meta":5,"style":5},"func TestCreateOrderE2E(t *testing.T) {\n    pool := setupPostgres(t)              \u002F\u002F testcontainers-go\n    repo := postgres.NewOrderRepo(pool)\n    uc := usecase.NewCreateOrder(repo, &fakePub{}, realClock{})\n    h := httpadapter.NewOrderHandler(uc, nil, nil)\n\n    e := echo.New()\n    e.POST(\"\u002Forders\", h.Create)\n\n    body := `{\"user_id\":\"u-1\",\"items\":[{\"sku\":\"A\",\"quantity\":2,\"price\":1000}]}`\n    req := httptest.NewRequest(http.MethodPost, \"\u002Forders\", strings.NewReader(body))\n    req.Header.Set(\"Content-Type\", \"application\u002Fjson\")\n    rec := httptest.NewRecorder()\n\n    e.ServeHTTP(rec, req)\n\n    require.Equal(t, http.StatusCreated, rec.Code)\n}\n",[20,7054,7055,7076,7091,7103,7126,7148,7152,7164,7179,7183,7193,7220,7240,7254,7258,7268,7272,7281],{"__ignoreMap":5},[308,7056,7057,7059,7062,7064,7066,7068,7070,7072,7074],{"class":310,"line":311},[308,7058,623],{"class":321},[308,7060,7061],{"class":325}," TestCreateOrderE2E",[308,7063,629],{"class":342},[308,7065,6697],{"class":632},[308,7067,1669],{"class":321},[308,7069,6353],{"class":325},[308,7071,604],{"class":342},[308,7073,6706],{"class":325},[308,7075,676],{"class":342},[308,7077,7078,7080,7082,7085,7088],{"class":310,"line":318},[308,7079,3609],{"class":342},[308,7081,767],{"class":321},[308,7083,7084],{"class":325}," setupPostgres",[308,7086,7087],{"class":342},"(t)              ",[308,7089,7090],{"class":314},"\u002F\u002F testcontainers-go\n",[308,7092,7093,7095,7097,7099,7101],{"class":310,"line":329},[308,7094,5180],{"class":342},[308,7096,767],{"class":321},[308,7098,4559],{"class":342},[308,7100,4562],{"class":325},[308,7102,4565],{"class":342},[308,7104,7105,7107,7109,7111,7113,7116,7118,7120,7122,7124],{"class":310,"line":336},[308,7106,6738],{"class":342},[308,7108,767],{"class":321},[308,7110,4613],{"class":342},[308,7112,4616],{"class":325},[308,7114,7115],{"class":342},"(repo, ",[308,7117,2898],{"class":321},[308,7119,6579],{"class":325},[308,7121,6985],{"class":342},[308,7123,4622],{"class":325},[308,7125,4625],{"class":342},[308,7127,7128,7131,7133,7135,7137,7140,7142,7144,7146],{"class":310,"line":346},[308,7129,7130],{"class":342},"    h ",[308,7132,767],{"class":321},[308,7134,4674],{"class":342},[308,7136,4677],{"class":325},[308,7138,7139],{"class":342},"(uc, ",[308,7141,1013],{"class":423},[308,7143,80],{"class":342},[308,7145,1013],{"class":423},[308,7147,387],{"class":342},[308,7149,7150],{"class":310,"line":359},[308,7151,333],{"emptyLinePlaceholder":332},[308,7153,7154,7156,7158,7160,7162],{"class":310,"line":369},[308,7155,4694],{"class":342},[308,7157,767],{"class":321},[308,7159,4699],{"class":342},[308,7161,1439],{"class":325},[308,7163,4422],{"class":342},[308,7165,7166,7168,7171,7173,7176],{"class":310,"line":374},[308,7167,4708],{"class":342},[308,7169,7170],{"class":325},"POST",[308,7172,629],{"class":342},[308,7174,7175],{"class":349},"\"\u002Forders\"",[308,7177,7178],{"class":342},", h.Create)\n",[308,7180,7181],{"class":310,"line":384},[308,7182,333],{"emptyLinePlaceholder":332},[308,7184,7185,7188,7190],{"class":310,"line":390},[308,7186,7187],{"class":342},"    body ",[308,7189,767],{"class":321},[308,7191,7192],{"class":349}," `{\"user_id\":\"u-1\",\"items\":[{\"sku\":\"A\",\"quantity\":2,\"price\":1000}]}`\n",[308,7194,7195,7198,7200,7203,7206,7209,7211,7214,7217],{"class":310,"line":395},[308,7196,7197],{"class":342},"    req ",[308,7199,767],{"class":321},[308,7201,7202],{"class":342}," httptest.",[308,7204,7205],{"class":325},"NewRequest",[308,7207,7208],{"class":342},"(http.MethodPost, ",[308,7210,7175],{"class":349},[308,7212,7213],{"class":342},", strings.",[308,7215,7216],{"class":325},"NewReader",[308,7218,7219],{"class":342},"(body))\n",[308,7221,7222,7225,7228,7230,7233,7235,7238],{"class":310,"line":407},[308,7223,7224],{"class":342},"    req.Header.",[308,7226,7227],{"class":325},"Set",[308,7229,629],{"class":342},[308,7231,7232],{"class":349},"\"Content-Type\"",[308,7234,80],{"class":342},[308,7236,7237],{"class":349},"\"application\u002Fjson\"",[308,7239,387],{"class":342},[308,7241,7242,7245,7247,7249,7252],{"class":310,"line":412},[308,7243,7244],{"class":342},"    rec ",[308,7246,767],{"class":321},[308,7248,7202],{"class":342},[308,7250,7251],{"class":325},"NewRecorder",[308,7253,4422],{"class":342},[308,7255,7256],{"class":310,"line":420},[308,7257,333],{"emptyLinePlaceholder":332},[308,7259,7260,7262,7265],{"class":310,"line":436},[308,7261,4708],{"class":342},[308,7263,7264],{"class":325},"ServeHTTP",[308,7266,7267],{"class":342},"(rec, req)\n",[308,7269,7270],{"class":310,"line":449},[308,7271,333],{"emptyLinePlaceholder":332},[308,7273,7274,7276,7278],{"class":310,"line":463},[308,7275,6874],{"class":342},[308,7277,6897],{"class":325},[308,7279,7280],{"class":342},"(t, http.StatusCreated, rec.Code)\n",[308,7282,7283],{"class":310,"line":476},[308,7284,533],{"class":342},[16,7286,7287],{},"Соотношение: ~80% быстрых unit-тестов на use case + domain, ~20% integration на адаптеры.",[11,7289,7291],{"id":7290},"практическая-навигация","Практическая навигация",[33,7293,7295],{"id":7294},"как-добавить-новый-use-case-пошагово","Как добавить новый use case (пошагово)",[7297,7298,7299,7305,7314,7319,7326,7332,7342,7348],"ol",{},[268,7300,7301,7302,604],{},"Если нужны новые правила домена — добавь метод\u002Fинвариант в ",[20,7303,7304],{},"internal\u002Fdomain\u002F...",[268,7306,7307,7308,7311,7312,604],{},"Создай файл ",[20,7309,7310],{},"internal\u002Fusecase\u002F\u003Cverb>_\u003Centity>.go",": Input, Output, struct, конструктор, ",[20,7313,2101],{},[268,7315,7316,7317,604],{},"Если use case требует нового порта — добавь интерфейс в ",[20,7318,125],{},[268,7320,7321,7322,7325],{},"Реализуй порт в ",[20,7323,7324],{},"internal\u002Fadapter\u002F..."," (postgres\u002Fkafka\u002Fhttp-client).",[268,7327,7328,7329,7331],{},"Добавь сборку в ",[20,7330,186],{}," (3–5 строк).",[268,7333,7334,7335,7338,7339,604],{},"Создай handler-метод в ",[20,7336,7337],{},"internal\u002Fadapter\u002Fhttp\u002Forder_handler.go"," + регистрация в ",[20,7340,7341],{},"router.go",[268,7343,7344,7345,604],{},"Маппинг ошибок — в ",[20,7346,7347],{},"error_mapper.go",[268,7349,7350],{},"Тесты: unit на use case + integration на handler.",[16,7352,7353],{},"Никакого редактирования божественных классов. Только дописывание.",[33,7355,7357],{"id":7356},"faq-куда-класть-x","FAQ — куда класть X",[51,7359,7360,7370],{},[54,7361,7362],{},[57,7363,7364,7367],{},[60,7365,7366],{},"Вопрос",[60,7368,7369],{},"Ответ",[70,7371,7372,7391,7407,7428,7443,7457,7471,7483,7498,7506,7517,7531],{},[57,7373,7374,7377],{},[75,7375,7376],{},"Куда положить валидацию входных данных?",[75,7378,7379,7380,80,7383,7386,7387,7390],{},"Базовая (",[20,7381,7382],{},"required",[20,7384,7385],{},"min",") — в request DTO у handler через ",[20,7388,7389],{},"validate:",". Бизнес-валидация (баланс, статус) — в domain или use case.",[57,7392,7393,7400],{},[75,7394,7395,7396,7399],{},"Куда класть ",[20,7397,7398],{},"time.Now()","?",[75,7401,7402,7403,7406],{},"За интерфейсом ",[20,7404,7405],{},"Clock",", передаваемым через DI. Иначе тесты будут хрупкими.",[57,7408,7409,7412],{},[75,7410,7411],{},"Где пишутся миграции?",[75,7413,7414,7417,7418,7420,7421,7424,7425,604],{},[20,7415,7416],{},"migrations\u002F"," в корне, вне ",[20,7419,219],{},". Прогоняются из ",[20,7422,7423],{},"cmd\u002Fmigrate"," или утилитой типа ",[20,7426,7427],{},"goose",[57,7429,7430,7433],{},[75,7431,7432],{},"Где живут константы лимитов (max items, min price)?",[75,7434,7435,7436,7439,7440,604],{},"Если бизнес-правило — в ",[20,7437,7438],{},"domain",". Если конфиг (timeout, retries) — в ",[20,7441,7442],{},"internal\u002Fconfig",[57,7444,7445,7448],{},[75,7446,7447],{},"Можно ли дернуть HTTP-клиент стороннего сервиса из use case?",[75,7449,7450,7451,7454,7455,604],{},"Только через интерфейс-порт. Реализация (",[20,7452,7453],{},"http.Client",") — в ",[20,7456,7324],{},[57,7458,7459,7462],{},[75,7460,7461],{},"Куда класть DTO для Kafka events?",[75,7463,7464,7465,7467,7468,604],{},"Структура события — в ",[20,7466,7438],{}," (это часть контракта). Маршаллинг и Producer — в ",[20,7469,7470],{},"adapter\u002Fkafka",[57,7472,7473,7476],{},[75,7474,7475],{},"Где маппинг row → entity?",[75,7477,7478,7479,7482],{},"В repository. Используй ",[20,7480,7481],{},"Hydrate(...)"," конструктор entity.",[57,7484,7485,7490],{},[75,7486,7487,7489],{},[20,7488,119],{}," — где интерфейс, где реализация?",[75,7491,7492,7493,7495,7496,604],{},"Интерфейс — в ",[20,7494,125],{}," (там, где он используется). Реализация — в ",[20,7497,140],{},[57,7499,7500,7503],{},[75,7501,7502],{},"Логи в domain — точно нельзя?",[75,7504,7505],{},"Лучше нет. Domain возвращает ошибки и события, а логирует слой выше.",[57,7507,7508,7511],{},[75,7509,7510],{},"Что делать, если два use case похожи на 90%?",[75,7512,7513,7514,7516],{},"Вынести общую часть в domain service или приватную функцию ",[20,7515,2597],{},". Не делать «BaseUseCase» с наследованием.",[57,7518,7519,7526],{},[75,7520,7521,206,7523,7525],{},[20,7522,4926],{},[20,7524,4929],{}," нужны?",[75,7527,7528,7529,604],{},"На малых проектах нет. На 100+ зависимостях — оправданы для уменьшения boilerplate в ",[20,7530,4237],{},[57,7532,7533,7540],{},[75,7534,7535,7536,7539],{},"Куда class ",[20,7537,7538],{},"User"," если им владеют 5 модулей?",[75,7541,7542,7543,7546],{},"Если есть общая концепция — в ",[20,7544,7545],{},"domain\u002Fuser",". Если у каждого модуля свой view — у каждого свой type. Не делай монолит-domain.",[33,7548,7550],{"id":7549},"подводные-камни","Подводные камни",[265,7552,7553,7562,7578,7588,7596,7602,7611,7633],{},[268,7554,7555,7558,7559,604],{},[5027,7556,7557],{},"Use case вызывает другой use case",". Признак пропущенного domain service. Выноси общее в ",[20,7560,7561],{},"domain\u002Forder\u002Fservice.go",[268,7563,7564,7571,7572,80,7575,243],{},[5027,7565,7566,7567,7570],{},"Repository принимает ",[20,7568,7569],{},"map[string]any"," фильтры",". Утечка ORM-стиля. Делай типизированные методы (",[20,7573,7574],{},"FindByUser",[20,7576,7577],{},"FindActiveSince",[268,7579,7580,7587],{},[5027,7581,7582,7583,7586],{},"Один большой ",[20,7584,7585],{},"internal\u002Fusecase\u002Forder.go"," со всеми сценариями",". Опять god-object, только в другой обёртке. Один файл — один сценарий.",[268,7589,7590,7595],{},[5027,7591,7592,7594],{},[20,7593,119],{}," объявлен в адаптере, use case импортирует адаптер",". Развернутая зависимость. Интерфейс должен жить там, где используется.",[268,7597,7598,7601],{},[5027,7599,7600],{},"Handler сам ходит в репозиторий «потому что тут только GET»",". Через год там будет бизнес-логика. Сразу через use case.",[268,7603,7604,7610],{},[5027,7605,7606,7609],{},[20,7607,7608],{},"*sql.DB"," в полях use case",". Обмазался Clean — а БД всё равно проникла. Только через порт.",[268,7612,7613,7619,7620,80,7623,80,7626,7629,7630,604],{},[5027,7614,7615,7616,7618],{},"Огромный ",[20,7617,4237],{}," на 800 строк",". Разбивай на ",[20,7621,7622],{},"wire.go",[20,7624,7625],{},"routes.go",[20,7627,7628],{},"init_db.go"," — но всё в ",[20,7631,7632],{},"cmd\u002Fserver\u002F",[268,7634,7635,7646,7647,7649,7650,7653],{},[5027,7636,7637,7638,7641,7642,7645],{},"Нет ",[20,7639,7640],{},"Hydrate()"," — entity создаётся через ",[20,7643,7644],{},"&Order{...}"," в repository",". Утечка приватных полей через unsafe-конструктор. Лучше явный ",[20,7648,7640],{},", чем ",[20,7651,7652],{},"New()"," в репо.",[33,7655,7657],{"id":7656},"чек-лист-ревью-pr","Чек-лист ревью PR",[265,7659,7662,7677,7693,7706,7716,7722,7731,7739,7745,7757],{"className":7660},[7661],"contains-task-list",[268,7663,7666,7670,7671,7674,7675],{"className":7664},[7665],"task-list-item",[7667,7668],"input",{"disabled":332,"type":7669},"checkbox"," Новый сценарий = новый файл в ",[20,7672,7673],{},"usecase\u002F"," с одним публичным ",[20,7676,2101],{},[268,7678,7680,7682,7683,80,7685,80,7688,80,7690],{"className":7679},[7665],[7667,7681],{"disabled":332,"type":7669}," Use case не импортирует ",[20,7684,2532],{},[20,7686,7687],{},"echo",[20,7689,5916],{},[20,7691,7692],{},"kafka-go",[268,7694,7696,7698,7699,206,7702,7705],{"className":7695},[7665],[7667,7697],{"disabled":332,"type":7669}," Domain не импортирует ничего, кроме stdlib (+ ",[20,7700,7701],{},"uuid",[20,7703,7704],{},"decimal"," ок)",[268,7707,7709,7711,7712,7715],{"className":7708},[7665],[7667,7710],{"disabled":332,"type":7669}," Repository возвращает ",[20,7713,7714],{},"*domain.X",", а не свой DTO",[268,7717,7719,7721],{"className":7718},[7665],[7667,7720],{"disabled":332,"type":7669}," Все интерфейсы (порты) — с context.Context первым параметром",[268,7723,7725,7727,7728,7730],{"className":7724},[7665],[7667,7726],{"disabled":332,"type":7669}," Ошибки из инфраструктуры либо обёрнуты ",[20,7729,2256],{},", либо транслированы в domain-ошибку",[268,7732,7734,7736,7737,5034],{"className":7733},[7665],[7667,7735],{"disabled":332,"type":7669}," Маппинг domain-ошибок в HTTP-коды — централизованный (",[20,7738,7347],{},[268,7740,7742,7744],{"className":7741},[7665],[7667,7743],{"disabled":332,"type":7669}," Юнит-тесты use case без поднятой БД",[268,7746,7748,7750,7751,206,7753,7756],{"className":7747},[7665],[7667,7749],{"disabled":332,"type":7669}," Никаких ",[20,7752,7398],{},[20,7754,7755],{},"uuid.New()"," напрямую в use case — через порт",[268,7758,7760,7762,7763,7765,7766,7769],{"className":7759},[7665],[7667,7761],{"disabled":332,"type":7669}," DI собрана в ",[20,7764,186],{},", а не в ",[20,7767,7768],{},"init()","-функциях",[11,7771,7773],{"id":7772},"вопросы-со-сборов","Вопросы со сборов",[7297,7775,7776,7791,7801,7813,7819,7825,7833,7839,7849,7858],{},[268,7777,7778,7784,7785,7787,7788,7790],{},[5027,7779,7780,7781,7783],{},"Где живёт интерфейс ",[20,7782,119],{}," — в domain, usecase или adapter?"," — Там, где он используется. Чаще всего — ",[20,7786,2597],{}," (или ",[20,7789,7438],{},", если репозиторий нужен domain service).",[268,7792,7793,7798,7799,604],{},[5027,7794,7795,7796,7399],{},"Зачем абстракция ",[20,7797,7405],{}," — Чтобы в тестах фиксировать время и не зависеть от ",[20,7800,7398],{},[268,7802,7803,7809,7810,7812],{},[5027,7804,7805,7806,7399],{},"Как сделать транзакцию на 3 операции в use case без утечки ",[20,7807,7808],{},"*sql.Tx"," — Через UnitOfWork\u002FTxManager-порт. Tx живёт в ",[20,7811,5130],{},", repository достаёт его через executor-helper.",[268,7814,7815,7818],{},[5027,7816,7817],{},"Куда положить outbox для надёжной отправки event?"," — Запись в БД в той же транзакции, что и order. Отдельный воркер читает таблицу outbox и публикует в Kafka.",[268,7820,7821,7824],{},[5027,7822,7823],{},"Что делать, если pgx-ошибка не маппится в нужный HTTP-код?"," — Repository транслирует pgx-ошибки в domain-ошибки. Handler маппит domain-ошибки в HTTP.",[268,7826,7827,7832],{},[5027,7828,7829,7830,7399],{},"Использовать ли ",[20,7831,4926],{}," — Зависит от размера. До 50 зависимостей — DI руками, понятнее и легче дебажить.",[268,7834,7835,7838],{},[5027,7836,7837],{},"Один use case = один файл — это догма?"," — Нет, но «один use case = один тип» — да. Группировать в один файл можно, если они тесно связаны и небольшие.",[268,7840,7841,7844,7845,7848],{},[5027,7842,7843],{},"Куда положить middleware (rate limit, JWT)?"," — ",[20,7846,7847],{},"internal\u002Fadapter\u002Fhttp\u002Fmiddleware\u002F",". Это инфраструктура транспорта, не бизнес.",[268,7850,7851,7854,7855,7857],{},[5027,7852,7853],{},"Что если use case надо вызвать из cron-воркера?"," — Use case не зависит от транспорта, поэтому просто дергай его ",[20,7856,2101],{}," из любого entrypoint.",[268,7859,7860,7863,7864,7867],{},[5027,7861,7862],{},"Как тестировать ошибку «БД упала»?"," — Мок-репо возвращает ",[20,7865,7866],{},"errors.New(\"conn failed\")",", проверяется что use case оборачивает её и возвращает наверх.",[7869,7870],"hr",{},[11,7872,7874],{"id":7873},"практика","Практика",[7876,7877,7879,7885,7902],"quiz",{"answer":6852,"id":7878,"xp":4839},"arch-clean-practice-q1",[16,7880,7881,7882,7884],{},"Где лучше держать интерфейс ",[20,7883,119],{},", если его использует use case?",[7886,7887,7888],"template",{"v-slot:options":5},[265,7889,7890,7893,7896,7899],{},[268,7891,7892],{},"В postgres-адаптере рядом с SQL",[268,7894,7895],{},"В usecase-пакете, где он нужен",[268,7897,7898],{},"В HTTP handler, потому что запрос приходит оттуда",[268,7900,7901],{},"В global\u002Finterfaces, чтобы все могли импортировать",[7886,7903,7904],{"v-slot:explanation":5},[16,7905,7906],{},"Порт принадлежит consumer-у. Use case формулирует, что ему нужно от хранилища, а adapter реализует этот контракт.",[7908,7909,7913,7916,8098],"predict",{"answer":7910,"id":7911,"xp":7912},"404\\n409\\n500","arch-clean-practice-p1","15",[16,7914,7915],{},"Что выведет программа?",[7886,7917,7918],{"v-slot:code":5},[38,7919,7921],{"className":302,"code":7920,"language":304,"meta":5,"style":5},"package main\n\nimport \"fmt\"\n\nfunc httpStatus(kind string) int {\n    switch kind {\n    case \"not_found\":\n        return 404\n    case \"conflict\":\n        return 409\n    default:\n        return 500\n    }\n}\n\nfunc main() {\n    fmt.Println(httpStatus(\"not_found\"))\n    fmt.Println(httpStatus(\"conflict\"))\n    fmt.Println(httpStatus(\"pgx_error\"))\n}\n",[20,7922,7923,7929,7933,7943,7947,7967,7974,7983,7990,7999,8006,8012,8019,8023,8027,8031,8039,8060,8077,8094],{"__ignoreMap":5},[308,7924,7925,7927],{"class":310,"line":311},[308,7926,322],{"class":321},[308,7928,4255],{"class":325},[308,7930,7931],{"class":310,"line":318},[308,7932,333],{"emptyLinePlaceholder":332},[308,7934,7935,7937,7939,7941],{"class":310,"line":329},[308,7936,339],{"class":321},[308,7938,1411],{"class":349},[308,7940,1859],{"class":325},[308,7942,356],{"class":349},[308,7944,7945],{"class":310,"line":336},[308,7946,333],{"emptyLinePlaceholder":332},[308,7948,7949,7951,7954,7956,7959,7961,7963,7965],{"class":310,"line":346},[308,7950,623],{"class":321},[308,7952,7953],{"class":325}," httpStatus",[308,7955,629],{"class":342},[308,7957,7958],{"class":632},"kind",[308,7960,636],{"class":321},[308,7962,924],{"class":342},[308,7964,2777],{"class":321},[308,7966,497],{"class":342},[308,7968,7969,7971],{"class":310,"line":359},[308,7970,3306],{"class":321},[308,7972,7973],{"class":342}," kind {\n",[308,7975,7976,7978,7981],{"class":310,"line":369},[308,7977,3313],{"class":321},[308,7979,7980],{"class":349}," \"not_found\"",[308,7982,3436],{"class":342},[308,7984,7985,7987],{"class":310,"line":374},[308,7986,699],{"class":321},[308,7988,7989],{"class":423}," 404\n",[308,7991,7992,7994,7997],{"class":310,"line":384},[308,7993,3313],{"class":321},[308,7995,7996],{"class":349}," \"conflict\"",[308,7998,3436],{"class":342},[308,8000,8001,8003],{"class":310,"line":390},[308,8002,699],{"class":321},[308,8004,8005],{"class":423}," 409\n",[308,8007,8008,8010],{"class":310,"line":395},[308,8009,3433],{"class":321},[308,8011,3436],{"class":342},[308,8013,8014,8016],{"class":310,"line":407},[308,8015,699],{"class":321},[308,8017,8018],{"class":423}," 500\n",[308,8020,8021],{"class":310,"line":412},[308,8022,711],{"class":342},[308,8024,8025],{"class":310,"line":420},[308,8026,533],{"class":342},[308,8028,8029],{"class":310,"line":436},[308,8030,333],{"emptyLinePlaceholder":332},[308,8032,8033,8035,8037],{"class":310,"line":449},[308,8034,623],{"class":321},[308,8036,4403],{"class":325},[308,8038,4406],{"class":342},[308,8040,8041,8044,8047,8049,8052,8054,8057],{"class":310,"line":463},[308,8042,8043],{"class":342},"    fmt.",[308,8045,8046],{"class":325},"Println",[308,8048,629],{"class":342},[308,8050,8051],{"class":325},"httpStatus",[308,8053,629],{"class":342},[308,8055,8056],{"class":349},"\"not_found\"",[308,8058,8059],{"class":342},"))\n",[308,8061,8062,8064,8066,8068,8070,8072,8075],{"class":310,"line":476},[308,8063,8043],{"class":342},[308,8065,8046],{"class":325},[308,8067,629],{"class":342},[308,8069,8051],{"class":325},[308,8071,629],{"class":342},[308,8073,8074],{"class":349},"\"conflict\"",[308,8076,8059],{"class":342},[308,8078,8079,8081,8083,8085,8087,8089,8092],{"class":310,"line":481},[308,8080,8043],{"class":342},[308,8082,8046],{"class":325},[308,8084,629],{"class":342},[308,8086,8051],{"class":325},[308,8088,629],{"class":342},[308,8090,8091],{"class":349},"\"pgx_error\"",[308,8093,8059],{"class":342},[308,8095,8096],{"class":310,"line":486},[308,8097,533],{"class":342},[7886,8099,8100],{"v-slot:hint":5},[16,8101,8102],{},"Инфраструктурные ошибки лучше транслировать в domain\u002Fusecase-ошибки, а HTTP-коды маппить централизованно.",[8104,8105,8109,8120,8248],"code-task",{"expected":8106,"id":8107,"xp":8108},"ok\\nbad\\nok","arch-clean-practice-ct1","20",[16,8110,8111,8112,8115,8116,8119],{},"Реализуй ",[20,8113,8114],{},"PortSignatureReview",": метод порта должен принимать ",[20,8117,8118],{},"context.Context"," первым аргументом.",[7886,8121,8122],{"v-slot:template":5},[38,8123,8125],{"className":302,"code":8124,"language":304,"meta":5,"style":5},"package main\n\nimport \"fmt\"\n\nfunc PortSignatureReview(firstArg string) string {\n    return \"bad\"\n}\n\nfunc main() {\n    fmt.Println(PortSignatureReview(\"context.Context\"))\n    fmt.Println(PortSignatureReview(\"int64\"))\n    fmt.Println(PortSignatureReview(\"context.Context\"))\n}\n",[20,8126,8127,8133,8137,8147,8151,8171,8178,8182,8186,8194,8211,8228,8244],{"__ignoreMap":5},[308,8128,8129,8131],{"class":310,"line":311},[308,8130,322],{"class":321},[308,8132,4255],{"class":325},[308,8134,8135],{"class":310,"line":318},[308,8136,333],{"emptyLinePlaceholder":332},[308,8138,8139,8141,8143,8145],{"class":310,"line":329},[308,8140,339],{"class":321},[308,8142,1411],{"class":349},[308,8144,1859],{"class":325},[308,8146,356],{"class":349},[308,8148,8149],{"class":310,"line":336},[308,8150,333],{"emptyLinePlaceholder":332},[308,8152,8153,8155,8158,8160,8163,8165,8167,8169],{"class":310,"line":346},[308,8154,623],{"class":321},[308,8156,8157],{"class":325}," PortSignatureReview",[308,8159,629],{"class":342},[308,8161,8162],{"class":632},"firstArg",[308,8164,636],{"class":321},[308,8166,924],{"class":342},[308,8168,933],{"class":321},[308,8170,497],{"class":342},[308,8172,8173,8175],{"class":310,"line":359},[308,8174,837],{"class":321},[308,8176,8177],{"class":349}," \"bad\"\n",[308,8179,8180],{"class":310,"line":369},[308,8181,533],{"class":342},[308,8183,8184],{"class":310,"line":374},[308,8185,333],{"emptyLinePlaceholder":332},[308,8187,8188,8190,8192],{"class":310,"line":384},[308,8189,623],{"class":321},[308,8191,4403],{"class":325},[308,8193,4406],{"class":342},[308,8195,8196,8198,8200,8202,8204,8206,8209],{"class":310,"line":390},[308,8197,8043],{"class":342},[308,8199,8046],{"class":325},[308,8201,629],{"class":342},[308,8203,8114],{"class":325},[308,8205,629],{"class":342},[308,8207,8208],{"class":349},"\"context.Context\"",[308,8210,8059],{"class":342},[308,8212,8213,8215,8217,8219,8221,8223,8226],{"class":310,"line":395},[308,8214,8043],{"class":342},[308,8216,8046],{"class":325},[308,8218,629],{"class":342},[308,8220,8114],{"class":325},[308,8222,629],{"class":342},[308,8224,8225],{"class":349},"\"int64\"",[308,8227,8059],{"class":342},[308,8229,8230,8232,8234,8236,8238,8240,8242],{"class":310,"line":407},[308,8231,8043],{"class":342},[308,8233,8046],{"class":325},[308,8235,629],{"class":342},[308,8237,8114],{"class":325},[308,8239,629],{"class":342},[308,8241,8208],{"class":349},[308,8243,8059],{"class":342},[308,8245,8246],{"class":310,"line":412},[308,8247,533],{"class":342},[7886,8249,8250],{"v-slot:hints":5},[265,8251,8252,8257],{},[268,8253,8254,8255],{},"В Go cancellation\u002Fdeadline должны идти через ",[20,8256,8118],{},[268,8258,8259,8260,8262],{},"Для портов правило простое: ",[20,8261,1653],{}," первым параметром",[8264,8265,8266],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":5,"searchDepth":318,"depth":318,"links":8268},[8269,8270,8276,8288,8298,8304,8305],{"id":13,"depth":318,"text":14},{"id":30,"depth":318,"text":31,"children":8271},[8272,8273,8274,8275],{"id":35,"depth":329,"text":36},{"id":48,"depth":329,"text":49},{"id":226,"depth":329,"text":227},{"id":246,"depth":329,"text":247},{"id":285,"depth":318,"text":286,"children":8277},[8278,8280,8282,8284,8286],{"id":295,"depth":329,"text":8279},"1. Domain — Order entity",{"id":1564,"depth":329,"text":8281},"2. Use Case — CreateOrder",{"id":2487,"depth":329,"text":8283},"3. HTTP handler — OrderHandler",{"id":3481,"depth":329,"text":8285},"4. Repository — PostgresOrderRepo",{"id":4233,"depth":329,"text":8287},"5. Сборка в main.go",{"id":4933,"depth":318,"text":4934,"children":8289},[8290,8291,8292,8293,8294,8295,8296,8297],{"id":4937,"depth":329,"text":4938},{"id":5056,"depth":329,"text":5057},{"id":5147,"depth":329,"text":5148},{"id":5311,"depth":329,"text":5312},{"id":5524,"depth":329,"text":5525},{"id":6306,"depth":329,"text":6307},{"id":6310,"depth":329,"text":6311},{"id":7048,"depth":329,"text":7049},{"id":7290,"depth":318,"text":7291,"children":8299},[8300,8301,8302,8303],{"id":7294,"depth":329,"text":7295},{"id":7356,"depth":329,"text":7357},{"id":7549,"depth":329,"text":7550},{"id":7656,"depth":329,"text":7657},{"id":7772,"depth":318,"text":7773},{"id":7873,"depth":318,"text":7874},1781022067241]