(args: ToolArgs, ctx: TrainingContext)
| 1278 | } |
| 1279 | |
| 1280 | export async function handleCreateMediaBuy(args: ToolArgs, ctx: TrainingContext) { |
| 1281 | const req = args as unknown as CreateMediaBuyRequest & ToolArgs; |
| 1282 | let session = await getSession(sessionKeyFromArgs(req, ctx.mode, ctx.userId, ctx.moduleId)); |
| 1283 | |
| 1284 | // Consume any single-shot directive registered by |
| 1285 | // comply_test_controller.force_create_media_buy_arm. Runs before all other |
| 1286 | // gates so the storyboard's wire-shape probe is not confounded by governance |
| 1287 | // or account-status checks; the directive is sandbox-only and the runner |
| 1288 | // explicitly opted into this response shape. Cleared after read — a second |
| 1289 | // create_media_buy from the same session resumes default behavior. |
| 1290 | // Idempotency_key replay is unaffected: the SDK's request-idempotency cache |
| 1291 | // wraps this handler, so a replayed request returns the cached submitted |
| 1292 | // response without re-evaluating the (now-empty) directive slot. |
| 1293 | const directive = session.complyExtensions.forcedCreateMediaBuyArm; |
| 1294 | if ( |
| 1295 | directive |
| 1296 | && directive.arm === 'submitted' |
| 1297 | && typeof directive.taskId === 'string' |
| 1298 | && directive.taskId.length > 0 |
| 1299 | && directive.taskId.length <= 128 |
| 1300 | ) { |
| 1301 | session.complyExtensions.forcedCreateMediaBuyArm = undefined; |
| 1302 | const responseMessage = |
| 1303 | typeof directive.message === 'string' && directive.message.length <= 2000 |
| 1304 | ? directive.message |
| 1305 | : undefined; |
| 1306 | return { |
| 1307 | status: 'submitted', |
| 1308 | task_id: directive.taskId, |
| 1309 | ...(responseMessage && { message: responseMessage }), |
| 1310 | }; |
| 1311 | } |
| 1312 | |
| 1313 | // Enforce account status gates set by comply_test_controller |
| 1314 | const accountId = (req as unknown as Record<string, unknown>).account as { account_id?: string } | undefined; |
| 1315 | if (accountId?.account_id) { |
| 1316 | const acctStatus = getAccountStatus(session, accountId.account_id); |
| 1317 | if (acctStatus && acctStatus !== 'active') { |
| 1318 | const BLOCKED_STATUSES: Record<string, string> = { |
| 1319 | suspended: 'Account is suspended — contact the seller to resolve.', |
| 1320 | payment_required: 'Account requires payment before new media buys can be created.', |
| 1321 | closed: 'Account is closed and cannot create new media buys.', |
| 1322 | rejected: 'Account was rejected and cannot create media buys.', |
| 1323 | }; |
| 1324 | return { |
| 1325 | errors: [{ code: 'ACCOUNT_STATUS_BLOCKED', message: BLOCKED_STATUSES[acctStatus] || `Account status "${acctStatus}" does not permit new media buys.` }] as TaskError[], |
| 1326 | }; |
| 1327 | } |
| 1328 | } |
| 1329 | |
| 1330 | // Enforce governance: if governance plans exist, validate the buy budget. |
| 1331 | // Deny-on-any-plan: without a governance_context there is no way to know |
| 1332 | // which plan the buy targets, so a conservative deny teaches buyers to call |
| 1333 | // check_governance first. |
| 1334 | const rawGovCtx = (req as unknown as Record<string, unknown>).governance_context; |
| 1335 | const govCtx = typeof rawGovCtx === 'string' && rawGovCtx ? rawGovCtx : undefined; |
| 1336 | if (govCtx) { |
| 1337 | // Find the latest check for this governance_context (Map iterates in insertion order) |
nothing calls this directly
no test coverage detected