summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-03-05 23:20:29 +0000
committersoryu <soryu@soryu.co>2026-03-07 02:27:41 +0000
commiteed949c692cbce0229d07f49fe974aa57699f305 (patch)
treeaeb3341567120e5736860468f12b37ea1bb62774
parent745b6f1b794e3d18f0ed42b1d261fc2bbcddb27e (diff)
downloadsoryu-eed949c692cbce0229d07f49fe974aa57699f305.tar.gz
soryu-eed949c692cbce0229d07f49fe974aa57699f305.zip
feat: soryu-co/soryu - makima: Add DOG database schema and backend CRUD
-rw-r--r--makima/src/server/handlers/directives.rs583
-rw-r--r--makima/src/server/handlers/mesh_supervisor.rs1
-rw-r--r--makima/src/server/handlers/orders.rs2
-rw-r--r--makima/src/server/mod.rs13
4 files changed, 597 insertions, 2 deletions
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs
index e6ccfa3..d1edf7e 100644
--- a/makima/src/server/handlers/directives.rs
+++ b/makima/src/server/handlers/directives.rs
@@ -9,11 +9,10 @@ use axum::{
use uuid::Uuid;
use crate::db::models::{
- CleanupResponse, CleanupTasksResponse, CreateDirectiveRequest, CreateTaskRequest,
+ CleanupResponse, CreateDirectiveRequest, CreateTaskRequest,
CreateDirectiveStepRequest, Directive, DirectiveListResponse,
DirectiveStep, DirectiveWithSteps, PickUpOrdersResponse,
UpdateDirectiveRequest, UpdateDirectiveStepRequest, UpdateGoalRequest,
- UpdateOrderRequest,
CreateDirectiveOrderGroupRequest, DirectiveOrderGroup,
DirectiveOrderGroupListResponse, UpdateDirectiveOrderGroupRequest,
OrderListResponse,
@@ -1378,3 +1377,583 @@ pub async fn pick_up_orders(
})
.into_response()
}
+
+// =============================================================================
+// Directive Order Group (DOG) CRUD
+// =============================================================================
+
+/// List all DOGs for a directive.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives/{id}/dogs",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ responses(
+ (status = 200, description = "List of DOGs", body = DirectiveOrderGroupListResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directive Order Groups"
+)]
+pub async fn list_dogs(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ match repository::list_directive_order_groups(pool, id, auth.owner_id).await {
+ Ok(dogs) => {
+ let total = dogs.len() as i64;
+ Json(DirectiveOrderGroupListResponse { dogs, total }).into_response()
+ }
+ Err(e) => {
+ tracing::error!("Failed to list DOGs: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("LIST_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Create a new DOG for a directive.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/dogs",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ request_body = CreateDirectiveOrderGroupRequest,
+ responses(
+ (status = 201, description = "DOG created", body = DirectiveOrderGroup),
+ (status = 400, description = "Invalid directive", body = ApiError),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directive Order Groups"
+)]
+pub async fn create_dog(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(req): Json<CreateDirectiveOrderGroupRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify the directive exists and belongs to this owner
+ match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(ApiError::new(
+ "INVALID_DIRECTIVE",
+ "directive_id must reference a valid directive owned by you",
+ )),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("VALIDATION_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::create_directive_order_group(pool, id, auth.owner_id, req).await {
+ Ok(dog) => (StatusCode::CREATED, Json(dog)).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to create DOG: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("CREATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Get a DOG by ID.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives/{id}/dogs/{dog_id}",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("dog_id" = Uuid, Path, description = "DOG ID"),
+ ),
+ responses(
+ (status = 200, description = "DOG details", body = DirectiveOrderGroup),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directive Order Groups"
+)]
+pub async fn get_dog(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, dog_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ let _ = id; // directive_id is in the path for REST nesting but we scope by owner_id
+
+ match repository::get_directive_order_group(pool, dog_id, auth.owner_id).await {
+ Ok(Some(dog)) => Json(dog).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "DOG not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to get DOG: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GET_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Update a DOG.
+#[utoipa::path(
+ patch,
+ path = "/api/v1/directives/{id}/dogs/{dog_id}",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("dog_id" = Uuid, Path, description = "DOG ID"),
+ ),
+ request_body = UpdateDirectiveOrderGroupRequest,
+ responses(
+ (status = 200, description = "DOG updated", body = DirectiveOrderGroup),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directive Order Groups"
+)]
+pub async fn update_dog(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, dog_id)): Path<(Uuid, Uuid)>,
+ Json(req): Json<UpdateDirectiveOrderGroupRequest>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ let _ = id; // directive_id is in the path for REST nesting but we scope by owner_id
+
+ // Validate status if provided
+ if let Some(ref status) = req.status {
+ if !["open", "in_progress", "done", "archived"].contains(&status.as_str()) {
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(ApiError::new(
+ "VALIDATION_FAILED",
+ "status must be one of: open, in_progress, done, archived",
+ )),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::update_directive_order_group(pool, dog_id, auth.owner_id, req).await {
+ Ok(Some(dog)) => Json(dog).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "DOG not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to update DOG: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("UPDATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Delete a DOG.
+#[utoipa::path(
+ delete,
+ path = "/api/v1/directives/{id}/dogs/{dog_id}",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("dog_id" = Uuid, Path, description = "DOG ID"),
+ ),
+ responses(
+ (status = 204, description = "Deleted"),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directive Order Groups"
+)]
+pub async fn delete_dog(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, dog_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ let _ = id; // directive_id is in the path for REST nesting but we scope by owner_id
+
+ match repository::delete_directive_order_group(pool, dog_id, auth.owner_id).await {
+ Ok(true) => StatusCode::NO_CONTENT.into_response(),
+ Ok(false) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "DOG not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to delete DOG: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DELETE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// List orders belonging to a specific DOG.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives/{id}/dogs/{dog_id}/orders",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("dog_id" = Uuid, Path, description = "DOG ID"),
+ ),
+ responses(
+ (status = 200, description = "List of orders in the DOG", body = OrderListResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directive Order Groups"
+)]
+pub async fn list_dog_orders(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, dog_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ let _ = id; // directive_id is in the path for REST nesting but we scope by owner_id
+
+ match repository::list_orders_by_dog(pool, dog_id, auth.owner_id).await {
+ Ok(orders) => {
+ let total = orders.len() as i64;
+ Json(OrderListResponse { orders, total }).into_response()
+ }
+ Err(e) => {
+ tracing::error!("Failed to list orders for DOG: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("LIST_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Pick up orders for a specific DOG. Like the directive pick-up-orders
+/// endpoint but filtered to orders belonging to the specified DOG.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/dogs/{dog_id}/pick-up-orders",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("dog_id" = Uuid, Path, description = "DOG ID"),
+ ),
+ responses(
+ (status = 200, description = "Orders picked up for planning", body = PickUpOrdersResponse),
+ (status = 404, description = "Directive or DOG not found", body = ApiError),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directive Order Groups"
+)]
+pub async fn pick_up_dog_orders(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, dog_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Verify directive ownership and get directive with steps
+ let (directive, mut steps) =
+ match repository::get_directive_with_steps_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some((d, s))) => (d, s),
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get directive: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ // Verify the DOG exists and belongs to this owner
+ match repository::get_directive_order_group(pool, dog_id, auth.owner_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "DOG not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ // Auto-remove completed steps that were already included in a PR
+ if directive.pr_url.is_some() || directive.pr_branch.is_some() {
+ match crate::orchestration::directive::remove_already_merged_steps(pool, id).await {
+ Ok(count) if count > 0 => {
+ tracing::info!("Auto-removed {} completed steps already in PR for directive {}", count, id);
+ steps = match repository::list_directive_steps(pool, id).await {
+ Ok(s) => s,
+ Err(e) => {
+ tracing::error!("Failed to re-fetch steps after cleanup: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("REFETCH_STEPS_FAILED", &e.to_string())),
+ ).into_response();
+ }
+ };
+ }
+ Err(e) => {
+ tracing::warn!("Failed to auto-remove merged steps for directive {}: {}", id, e);
+ }
+ _ => {}
+ }
+ }
+
+ // Reconcile existing orders
+ match repository::reconcile_directive_orders(pool, auth.owner_id, id).await {
+ Ok(count) => {
+ if count > 0 {
+ tracing::info!("Reconciled {} orders for directive {}", count, id);
+ }
+ }
+ Err(e) => {
+ tracing::warn!("Failed to reconcile directive orders: {}", e);
+ }
+ }
+
+ // Fetch available orders filtered to this DOG
+ let orders = match repository::get_available_orders_for_dog_pickup(pool, auth.owner_id, id, dog_id).await {
+ Ok(o) => o,
+ Err(e) => {
+ tracing::error!("Failed to fetch available orders for DOG: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("FETCH_ORDERS_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ // If no orders available, return early
+ if orders.is_empty() {
+ return Json(PickUpOrdersResponse {
+ message: "No orders available to plan for this DOG".to_string(),
+ order_count: 0,
+ task_id: None,
+ })
+ .into_response();
+ }
+
+ let order_count = orders.len() as i64;
+ let order_ids: Vec<Uuid> = orders.iter().map(|o| o.id).collect();
+
+ // Get generation and goal history for the planning prompt
+ let generation =
+ match repository::get_directive_max_generation(pool, id).await {
+ Ok(g) => g + 1,
+ Err(e) => {
+ tracing::error!("Failed to get max generation: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("GENERATION_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ let goal_history = match repository::get_directive_goal_history(pool, id, 3).await {
+ Ok(h) => h,
+ Err(e) => {
+ tracing::warn!("Failed to get goal history: {}", e);
+ vec![]
+ }
+ };
+
+ // Build the specialized planning prompt
+ let plan = build_order_pickup_prompt(&directive, &steps, &orders, generation, &goal_history);
+
+ // Link orders to the directive
+ if let Err(e) =
+ repository::bulk_link_orders_to_directive(pool, auth.owner_id, &order_ids, id).await
+ {
+ tracing::error!("Failed to link orders to directive: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("LINK_ORDERS_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+
+ // Mark picked-up orders as in_progress
+ if let Err(e) =
+ repository::bulk_update_order_status(pool, auth.owner_id, &order_ids, "in_progress").await
+ {
+ tracing::warn!("Failed to update order status to in_progress: {}", e);
+ }
+
+ // Create the planning task
+ let req = CreateTaskRequest {
+ contract_id: None,
+ name: format!("Pick up DOG orders: {}", directive.title),
+ description: Some("Directive order group pickup planning task".to_string()),
+ plan,
+ parent_task_id: None,
+ is_supervisor: false,
+ priority: 0,
+ repository_url: directive.repository_url.clone(),
+ base_branch: directive.base_branch.clone(),
+ target_branch: None,
+ merge_mode: None,
+ target_repo_path: None,
+ completion_action: None,
+ continue_from_task_id: None,
+ copy_files: None,
+ checkpoint_sha: None,
+ branched_from_task_id: None,
+ conversation_history: None,
+ supervisor_worktree_task_id: None,
+ directive_id: Some(directive.id),
+ directive_step_id: None,
+ };
+
+ let task = match repository::create_task_for_owner(pool, auth.owner_id, req).await {
+ Ok(t) => t,
+ Err(e) => {
+ tracing::error!("Failed to create DOG pickup planning task: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("CREATE_TASK_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ // Assign as orchestrator task
+ if let Err(e) = repository::assign_orchestrator_task(pool, id, task.id).await {
+ tracing::error!("Failed to assign orchestrator task: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("ASSIGN_TASK_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+
+ // Cancel old planning tasks
+ let cancelled = repository::cancel_old_planning_tasks(pool, id, task.id).await;
+ if let Ok(count) = cancelled {
+ if count > 0 {
+ tracing::info!(
+ directive_id = %id,
+ cancelled_count = count,
+ "Cancelled old planning tasks superseded by DOG order pickup"
+ );
+ }
+ }
+
+ // Set directive to active if draft/idle/paused
+ match directive.status.as_str() {
+ "draft" | "idle" | "paused" => {
+ if let Err(e) = repository::set_directive_status(pool, auth.owner_id, id, "active").await
+ {
+ tracing::warn!("Failed to set directive status to active: {}", e);
+ }
+ }
+ _ => {}
+ }
+
+ // Advance ready steps
+ let _ = repository::advance_directive_ready_steps(pool, id).await;
+
+ Json(PickUpOrdersResponse {
+ message: format!("Planning {} orders from DOG", order_count),
+ order_count,
+ task_id: Some(task.id),
+ })
+ .into_response()
+}
diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs
index 9d2dce7..ebde52b 100644
--- a/makima/src/server/handlers/mesh_supervisor.rs
+++ b/makima/src/server/handlers/mesh_supervisor.rs
@@ -3222,6 +3222,7 @@ pub async fn create_order_for_task(
labels: request.labels,
directive_id,
repository_url,
+ dog_id: None,
};
match repository::create_order(pool, owner_id, order_req).await {
diff --git a/makima/src/server/handlers/orders.rs b/makima/src/server/handlers/orders.rs
index 1251f79..03719cb 100644
--- a/makima/src/server/handlers/orders.rs
+++ b/makima/src/server/handlers/orders.rs
@@ -32,6 +32,7 @@ use crate::server::state::SharedState;
("type" = Option<String>, Query, description = "Filter by order type"),
("priority" = Option<String>, Query, description = "Filter by priority"),
("directive_id" = Option<Uuid>, Query, description = "Filter by directive ID"),
+ ("dog_id" = Option<Uuid>, Query, description = "Filter by DOG (Directive Order Group) ID"),
("search" = Option<String>, Query, description = "Text search across title, description, and directive name"),
),
responses(
@@ -62,6 +63,7 @@ pub async fn list_orders(
query.order_type.as_deref(),
query.priority.as_deref(),
query.directive_id,
+ query.dog_id,
query.search.as_deref(),
)
.await
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index b84b90e..6321518 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -249,6 +249,19 @@ pub fn make_router(state: SharedState) -> Router {
.route("/directives/{id}/cleanup", post(directives::cleanup_directive))
.route("/directives/{id}/create-pr", post(directives::create_pr))
.route("/directives/{id}/pick-up-orders", post(directives::pick_up_orders))
+ // Directive Order Group (DOG) endpoints
+ .route(
+ "/directives/{id}/dogs",
+ get(directives::list_dogs).post(directives::create_dog),
+ )
+ .route(
+ "/directives/{id}/dogs/{dog_id}",
+ get(directives::get_dog)
+ .patch(directives::update_dog)
+ .delete(directives::delete_dog),
+ )
+ .route("/directives/{id}/dogs/{dog_id}/orders", get(directives::list_dog_orders))
+ .route("/directives/{id}/dogs/{dog_id}/pick-up-orders", post(directives::pick_up_dog_orders))
// Order endpoints
.route(
"/orders",