Repository: akash-network/console
Pull request #1364: refactor: move /v1/providers/{address}/deployments to modules
Author: login: jzsfkzm
Created: 2025-05-25T19:16:01Z | Merged: 2025-05-28T02:08:18Z

refs #1272

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a new API endpoint to fetch paginated deployments for a provider, supporting optional status filtering ("active" or "closed").
  - Deployment responses include detailed information on leases, resources, status, timestamps, and balances.

- **Bug Fixes**
  - Enhanced validation and error responses for invalid deployment status filters.

- **Tests**
  - Added functional tests covering provider deployments API, verifying pagination, filtering, and data accuracy.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts
index 2d5e57b27b..0c5c1a1dfb 100644
--- a/apps/api/src/app.ts
+++ b/apps/api/src/app.ts
@@ -47,6 +47,7 @@ import {
   auditorsRouter,
   providerAttributesSchemaRouter,
   providerDashboardRouter,
+  providerDeploymentsRouter,
   providerGraphDataRouter,
   providerRegionsRouter,
   providersRouter,
@@ -114,6 +115,7 @@ appHono.route("/", providerRegionsRouter);
 appHono.route("/", providerDashboardRouter);
 appHono.route("/", providerVersionsRouter);
 appHono.route("/", providerGraphDataRouter);
+appHono.route("/", providerDeploymentsRouter);
 
 appHono.route("/", notificationsApiProxy);
 
diff --git a/apps/api/src/provider/controllers/provider-deployments/provider-deployments.controller.ts b/apps/api/src/provider/controllers/provider-deployments/provider-deployments.controller.ts
new file mode 100644
index 0000000000..2358f2e39e
--- /dev/null
+++ b/apps/api/src/provider/controllers/provider-deployments/provider-deployments.controller.ts
@@ -0,0 +1,26 @@
+import { singleton } from "tsyringe";
+
+import { ProviderDeploymentsQuery, ProviderDeploymentsResponse } from "@src/provider/http-schemas/provider-deployments.schema";
+import { ProviderDeploymentsService } from "@src/provider/services/provider-deployments/provider-deployments.service";
+
+@singleton()
+export class ProviderDeploymentsController {
+  constructor(private readonly providerDeploymentsService: ProviderDeploymentsService) {}
+
+  async getProviderDeployments(
+    provider: string,
+    skip: number,
+    limit: number,
+    status?: ProviderDeploymentsQuery["status"]
+  ): Promise<ProviderDeploymentsResponse> {
+    const deploymentCountQuery = this.providerDeploymentsService.getProviderDeploymentsCount(provider, status);
+    const deploymentsQuery = this.providerDeploymentsService.getProviderDeployments(provider, skip, limit, status);
+
+    const [deploymentCount, deployments] = await Promise.all([deploymentCountQuery, deploymentsQuery]);
+
+    return {
+      total: deploymentCount,
+      deployments: deployments
+    };
+  }
+}
diff --git a/apps/api/src/provider/http-schemas/provider-deployments.schema.ts b/apps/api/src/provider/http-schemas/provider-deployments.schema.ts
new file mode 100644
index 0000000000..78bd88f9da
--- /dev/null
+++ b/apps/api/src/provider/http-schemas/provider-deployments.schema.ts
@@ -0,0 +1,80 @@
+import { z } from "zod";
+
+import { openApiExampleProviderAddress } from "@src/utils/constants";
+
+const maxLimit = 100;
+
+export const ProviderDeploymentsParamsSchema = z.object({
+  provider: z.string().openapi({
+    description: "Provider Address",
+    example: openApiExampleProviderAddress
+  }),
+  skip: z.coerce.number().min(0).openapi({
+    description: "Deployments to skip",
+    example: 10
+  }),
+  limit: z.coerce.number().min(1).max(maxLimit).openapi({
+    description: "Deployments to return",
+    example: 10,
+    maximum: maxLimit
+  })
+});
+
+export const ProviderDeploymentsQuerySchema = z.object({
+  status: z
+    .enum(["active", "closed"])
+    .optional()
+    .openapi({
+      description: "Filter by status",
+      enum: ["active", "closed"],
+      example: "closed"
+    })
+});
+
+export const ProviderDeploymentsResponseSchema = z.object({
+  total: z.number(),
+  deployments: z.array(
+    z.object({
+      owner: z.string(),
+      dseq: z.string(),
+      denom: z.string(),
+      createdHeight: z.number(),
+      createdDate: z.date().nullable(),
+      status: z.string(), //TODO
+      balance: z.number(),
+      transferred: z.number(),
+      settledAt: z.number().nullable(),
+      resources: z.object({
+        cpu: z.number(),
+        memory: z.number(),
+        gpu: z.number(),
+        ephemeralStorage: z.number(),
+        persistentStorage: z.number()
+      }),
+      leases: z.array(
+        z.object({
+          provider: z.string(),
+          gseq: z.number(),
+          oseq: z.number(),
+          price: z.number(),
+          createdHeight: z.number(),
+          createdDate: z.date().nullable(),
+          closedHeight: z.number().nullable(),
+          closedDate: z.date().nullable(),
+          status: z.string(), //TODO
+          resources: z.object({
+            cpu: z.number(),
+            memory: z.number(),
+            gpu: z.number(),
+            ephemeralStorage: z.number(),
+            persistentStorage: z.number()
+          })
+        })
+      )
+    })
+  )
+});
+
+export type ProviderDeploymentsParams = z.infer<typeof ProviderDeploymentsParamsSchema>;
+export type ProviderDeploymentsQuery = z.infer<typeof ProviderDeploymentsQuerySchema>;
+export type ProviderDeploymentsResponse = z.infer<typeof ProviderDeploymentsResponseSchema>;
diff --git a/apps/api/src/provider/routes/index.ts b/apps/api/src/provider/routes/index.ts
index 7125c06fad..09ba35f164 100644
--- a/apps/api/src/provider/routes/index.ts
+++ b/apps/api/src/provider/routes/index.ts
@@ -5,3 +5,4 @@ export * from "@src/provider/routes/auditors/auditors.router";
 export * from "@src/provider/routes/provider-dashboard/provider-dashboard.router";
 export * from "@src/provider/routes/provider-versions/provider-versions.router";
 export * from "@src/provider/routes/provider-graph-data/provider-graph-data.router";
+export * from "@src/provider/routes/provider-deployments/provider-deployments.router";
diff --git a/apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts b/apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts
new file mode 100644
index 0000000000..b9388af32a
--- /dev/null
+++ b/apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts
@@ -0,0 +1,44 @@
+import { createRoute } from "@hono/zod-openapi";
+import { container } from "tsyringe";
+
+import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler";
+import { ProviderDeploymentsController } from "@src/provider/controllers/provider-deployments/provider-deployments.controller";
+import {
+  ProviderDeploymentsParamsSchema,
+  ProviderDeploymentsQuerySchema,
+  ProviderDeploymentsResponseSchema
+} from "@src/provider/http-schemas/provider-deployments.schema";
+
+const route = createRoute({
+  method: "get",
+  path: "/v1/providers/{provider}/deployments/{skip}/{limit}",
+  summary: "Get a list of deployments for a provider.",
+  tags: ["Providers", "Deployments"],
+  request: {
+    params: ProviderDeploymentsParamsSchema,
+    query: ProviderDeploymentsQuerySchema
+  },
+  responses: {
+    200: {
+      description: "Returns deployment list",
+      content: {
+        "application/json": {
+          schema: ProviderDeploymentsResponseSchema
+        }
+      }
+    },
+    400: {
+      description: "Invalid status filter"
+    }
+  }
+});
+
+export const providerDeploymentsRouter = new OpenApiHonoHandler();
+
+providerDeploymentsRouter.openapi(route, async function routeProviderDeployments(c) {
+  const { provider, skip, limit } = c.req.valid("param");
+  const { status } = c.req.valid("query");
+
+  const response = await container.resolve(ProviderDeploymentsController).getProviderDeployments(provider, skip, limit, status);
+  return c.json(response);
+});
diff --git a/apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts b/apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts
new file mode 100644
index 0000000000..9ac36960b6
--- /dev/null
+++ b/apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts
@@ -0,0 +1,104 @@
+import { Block } from "@akashnetwork/database/dbSchemas";
+import { Deployment, Lease } from "@akashnetwork/database/dbSchemas/akash";
+import type { WhereOptions } from "sequelize";
+import { Op } from "sequelize";
+import { singleton } from "tsyringe";
+
+import { ProviderDeploymentsQuery } from "@src/provider/http-schemas/provider-deployments.schema";
+
+@singleton()
+export class ProviderDeploymentsService {
+  async getProviderDeploymentsCount(provider: string, status?: ProviderDeploymentsQuery["status"]) {
+    const leaseFilter: WhereOptions<Lease> = { providerAddress: provider };
+    if (status) {
+      leaseFilter.closedHeight = status === "active" ? null : { [Op.ne]: null };
+    }
+
+    const result = await Deployment.count({
+      include: [{ model: Lease, attributes: [], required: true, where: leaseFilter }]
+    });
+
+    return result;
+  }
+
+  async getProviderDeployments(provider: string, skip: number, limit: number, status?: ProviderDeploymentsQuery["status"]) {
+    const leaseFilter: WhereOptions<Lease> = { providerAddress: provider };
+
+    if (status) {
+      leaseFilter.closedHeight = status === "active" ? null : { [Op.ne]: null };
+    }
+
+    const deploymentDseqs = await Deployment.findAll({
+      attributes: ["dseq", "createdHeight"],
+      include: [{ model: Lease, attributes: [], required: true, where: leaseFilter }],
+      order: [
+        ["createdHeight", "DESC"],
+        ["dseq", "DESC"]
+      ],
+      offset: skip,
+      limit: limit
+    });
+
+    const deployments = await Deployment.findAll({
+      where: {
+        dseq: { [Op.in]: deploymentDseqs.map(d => d.dseq) }
+      },
+      include: [
+        {
+          model: Lease,
+          required: true,
+          where: { providerAddress: provider },
+          include: [
+            { model: Block, required: true, as: "createdBlock" },
+            { model: Block, required: false, as: "closedBlock" }
+          ]
+        },
+        { model: Block, required: true, as: "createdBlock" },
+        { model: Block, required: false, as: "closedBlock" }
+      ],
+      order: [["createdHeight", "DESC"]]
+    });
+
+    return deployments.map(d => ({
+      owner: d.owner,
+      dseq: d.dseq,
+      denom: d.denom,
+      createdHeight: d.createdHeight,
+      createdDate: d.createdBlock.datetime,
+      closedHeight: d.closedHeight,
+      closedDate: d.closedHeight && d.closedBlock ? d.closedBlock.datetime : null,
+      status: d.closedHeight ? "closed" : "active",
+      balance: d.balance,
+      transferred: d.withdrawnAmount,
+      settledAt: d.lastWithdrawHeight,
+      resources: d.leases.reduce(
+        (acc, l) => ({
+          cpu: acc.cpu + l.cpuUnits,
+          memory: acc.memory + l.memoryQuantity,
+          gpu: acc.gpu + l.gpuUnits,
+          ephemeralStorage: acc.ephemeralStorage + l.ephemeralStorageQuantity,
+          persistentStorage: acc.persistentStorage + l.persistentStorageQuantity
+        }),
+        { cpu: 0, memory: 0, gpu: 0, ephemeralStorage: 0, persistentStorage: 0 }
+      ),
+      leases: d.leases.map(l => ({
+        provider: l.providerAddress,
+        gseq: l.gseq,
+        oseq: l.oseq,
+        price: l.price,
+        createdHeight: l.createdHeight,
+        createdDate: l.createdBlock.datetime,
+        closedHeight: l.closedHeight,
+        closedDate: l.closedHeight && l.closedBlock ? l.closedBlock.datetime : null,
+        status: l.closedHeight ? "closed" : "active",
+        resources: {
+          cpu: l.cpuUnits,
+          memory: l.memoryQuantity,
+          gpu: l.gpuUnits,
+          ephemeralStorage: l.ephemeralStorageQuantity,
+          persistentStorage: l.persistentStorageQuantity
+        }
+      }))
+    }));
+  }
+}
diff --git a/apps/api/src/routes/v1/index.ts b/apps/api/src/routes/v1/index.ts
index 8bdd835bba..545462157a 100644
--- a/apps/api/src/routes/v1/index.ts
+++ b/apps/api/src/routes/v1/index.ts
@@ -8,7 +8,6 @@ import nodesSandbox from "./nodes/sandbox";
 import nodesTestnet from "./nodes/testnet";
 import proposalById from "./proposals/byId";
 import proposals from "./proposals/list";
-import providerDeployments from "./providers/deployments";
 import templateById from "./templates/byId";
 import templateList from "./templates/list";
 import templateListFull from "./templates/list-full";
@@ -42,7 +41,6 @@ export default [
   transactionByHash,
   address,
   addressTransactions,
-  providerDeployments,
   validators,
   validatorByAddress,
   proposals,
diff --git a/apps/api/src/routes/v1/providers/deployments.ts b/apps/api/src/routes/v1/providers/deployments.ts
deleted file mode 100644
index 9d7319b0e4..0000000000
--- a/apps/api/src/routes/v1/providers/deployments.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
-
-import { getProviderDeployments, getProviderDeploymentsCount } from "@src/services/db/deploymentService";
-import { openApiExampleProviderAddress } from "@src/utils/constants";
-
-const maxLimit = 100;
-
-const route = createRoute({
-  method: "get",
-  path: "/providers/{provider}/deployments/{skip}/{limit}",
-  summary: "Get a list of deployments for a provider.",
-  tags: ["Providers", "Deployments"],
-  request: {
-    params: z.object({
-      provider: z.string().openapi({
-        description: "Provider Address",
-        example: openApiExampleProviderAddress
-      }),
-      skip: z.string().openapi({
-        description: "Deployments to skip",
-        example: "10"
-      }),
-      limit: z.string().openapi({
-        description: "Deployments to return",
-        example: "10",
-        maximum: maxLimit
-      })
-    }),
-    query: z.object({
-      status: z
-        .string()
-        .optional()
-        .openapi({
-          description: "Filter by status",
-          enum: ["active", "closed"],
-          example: "closed"
-        })
-    })
-  },
-  responses: {
-    200: {
-      description: "Returns deployment list",
-      content: {
-        "application/json": {
-          schema: z.array(
-            z.object({
-              owner: z.string(),
-              dseq: z.string(),
-              denom: z.string(),
-              createdHeight: z.number(),
-              createdDate: z.string().nullable(),
-              status: z.string(), //TODO
-              balance: z.number(),
-              transferred: z.number(),
-              settledAt: z.number().nullable(),
-              resources: z.object({
-                cpu: z.number(),
-                memory: z.number(),
-                gpu: z.number(),
-                ephemeralStorage: z.number(),
-                persistentStorage: z.number()
-              }),
-              leases: z.array(
-                z.object({
-                  provider: z.string(),
-                  gseq: z.number(),
-                  oseq: z.number(),
-                  price: z.number(),
-                  createdHeight: z.number(),
-                  createdDate: z.string().nullable(),
-                  closedHeight: z.number().nullable(),
-                  closedDate: z.string().nullable(),
-                  status: z.string(), //TODO
-                  resources: z.object({
-                    cpu: z.number(),
-                    memory: z.number(),
-                    gpu: z.number(),
-                    ephemeralStorage: z.number(),
-                    persistentStorage: z.number()
-                  })
-                })
-              )
-            })
-          )
-        }
-      }
-    },
-    400: {
-      description: "Invalid status filter"
-    }
-  }
-});
-
-export default new OpenAPIHono().openapi(route, async c => {
-  const skip = parseInt(c.req.valid("param").skip);
-  const limit = Math.min(maxLimit, parseInt(c.req.valid("param").limit));
-  const statusParam = c.req.query("status");
-  // TODO: Validate skip/limit
-  if (statusParam && statusParam !== "active" && statusParam !== "closed") {
-    return c.text(`Invalid status filter: "${statusParam}". Valid values are "active" and "closed".`, 400);
-  }
-
-  const deploymentCountQuery = getProviderDeploymentsCount(c.req.valid("param").provider, statusParam as "active" | "closed");
-  const deploymentsQuery = getProviderDeployments(c.req.valid("param").provider, skip, limit, statusParam as "active" | "closed");
-
-  const [deploymentCount, deployments] = await Promise.all([deploymentCountQuery, deploymentsQuery]);
-
-  return c.json({
-    total: deploymentCount,
-    deployments: deployments
-  });
-});
diff --git a/apps/api/src/services/db/deploymentService.ts b/apps/api/src/services/db/deploymentService.ts
index 3922020467..0600adf8cb 100644
--- a/apps/api/src/services/db/deploymentService.ts
+++ b/apps/api/src/services/db/deploymentService.ts
@@ -1,9 +1,8 @@
 import type * as v2beta2 from "@akashnetwork/akash-api/akash/market/v1beta2";
 import type * as v1beta1 from "@akashnetwork/akash-api/deprecated/akash/market/v1beta1";
 import { Block, Message } from "@akashnetwork/database/dbSchemas";
-import { Deployment, Lease } from "@akashnetwork/database/dbSchemas/akash";
+import { Deployment } from "@akashnetwork/database/dbSchemas/akash";
 import { Transaction } from "@akashnetwork/database/dbSchemas/base";
-import type { WhereOptions } from "sequelize";
 import { Op } from "sequelize";
 
 import { decodeMsg } from "@src/utils/protobuf";
@@ -71,93 +70,3 @@ export async function getDeploymentRelatedMessages(owner: string, dseq: string)
     type: msg.type
   }));
 }
-
-export async function getProviderDeploymentsCount(provider: string, status?: "active" | "closed") {
-  const leaseFilter: WhereOptions<Lease> = { providerAddress: provider };
-  if (status) {
-    const op = status === "active" ? Op.is : Op.ne;
-    leaseFilter.closedHeight = { [op]: null };
-  }
-
-  const result = await Deployment.count({
-    include: [{ model: Lease, attributes: [], required: true, where: leaseFilter }]
-  });
-
-  return result;
-}
-
-export async function getProviderDeployments(provider: string, skip: number, limit: number, status?: "active" | "closed") {
-  const leaseFilter: WhereOptions<Lease> = { providerAddress: provider };
-
-  if (status) {
-    const op = status === "active" ? Op.is : Op.ne;
-    leaseFilter.closedHeight = { [op]: null };
-  }
-
-  const deploymentDseqs = await Deployment.findAll({
-    attributes: ["dseq", "createdHeight"],
-    include: [{ model: Lease, attributes: [], required: true, where: leaseFilter }],
-    order: [["createdHeight", "DESC"]],
-    offset: skip,
-    limit: limit
-  });
-
-  const deployments = await Deployment.findAll({
-    where: {
-      dseq: { [Op.in]: deploymentDseqs.map(d => d.dseq) }
-    },
-    include: [
-      {
-        model: Lease,
-        required: true,
-        where: { providerAddress: provider },
-        include: [
-          { model: Block, required: true, as: "createdBlock" },
-          { model: Block, required: false, as: "closedBlock" }
-        ]
-      },
-      { model: Block, required: true, as: "createdBlock" },
-      { model: Block, required: false, as: "closedBlock" }
-    ],
-    order: [["createdHeight", "DESC"]]
-  });
-
-  return deployments.map(d => ({
-    owner: d.owner,
-    dseq: d.dseq,
-    denom: d.denom,
-    createdHeight: d.createdHeight,
-    createdDate: d.createdBlock.datetime,
-    closedHeight: d.closedHeight,
-    closedDate: d.closedHeight ? d.closedBlock.datetime : null,
-    status: d.closedHeight ? "closed" : "active",
-    balance: d.balance,
-    transferred: d.withdrawnAmount,
-    settledAt: d.lastWithdrawHeight,
-    resources: {
-      cpu: d.leases.reduce((acc, l) => acc + l.cpuUnits, 0),
-      memory: d.leases.reduce((acc, l) => acc + l.memoryQuantity, 0),
-      gpu: d.leases.reduce((acc, l) => acc + l.gpuUnits, 0),
-      ephemeralStorage: d.leases.reduce((acc, l) => acc + l.ephemeralStorageQuantity, 0),
-      persistentStorage: d.leases.reduce((acc, l) => acc + l.persistentStorageQuantity, 0)
-    },
-    leases: d.leases.map(l => ({
-      provider: l.providerAddress,
-      gseq: l.gseq,
-      oseq: l.oseq,
-      price: l.price,
-      createdHeight: l.createdHeight,
-      createdDate: l.createdBlock.datetime,
-      closedHeight: l.closedHeight,
-      closedDate: l.closedHeight ? l.closedBlock.datetime : null,
-      status: l.closedHeight ? "closed" : "active",
-      resources: {
-        cpu: l.cpuUnits,
-        memory: l.memoryQuantity,
-        gpu: l.gpuUnits,
-        ephemeralStorage: l.ephemeralStorageQuantity,
-        persistentStorage: l.persistentStorageQuantity
-      }
-    }))
-  }));
-}
diff --git a/apps/api/test/functional/provider-deployments.spec.ts b/apps/api/test/functional/provider-deployments.spec.ts
new file mode 100644
index 0000000000..71bd5f3c87
--- /dev/null
+++ b/apps/api/test/functional/provider-deployments.spec.ts
@@ -0,0 +1,167 @@
+import type { Deployment, Provider } from "@akashnetwork/database/dbSchemas/akash";
+import map from "lodash/map";
+import mcache from "memory-cache";
+import nock from "nock";
+
+import { app, initDb } from "@src/app";
+import { closeConnections } from "@src/db/dbConnection";
+
+import { BlockSeeder } from "@test/seeders/block.seeder";
+import { DaySeeder } from "@test/seeders/day.seeder";
+import { DeploymentSeeder } from "@test/seeders/deployment.seeder";
+import { DeploymentGroupSeeder } from "@test/seeders/deployment-group.seeder";
+import { LeaseSeeder } from "@test/seeders/lease.seeder";
+import { ProviderSeeder } from "@test/seeders/provider.seeder";
+
+describe("Provider deployments", () => {
+  let provider: Provider;
+  let deployments: Deployment[];
+
+  beforeAll(async () => {
+    await initDb();
+
+    const day = await DaySeeder.createInDatabase({
+      date: new Date(),
+      firstBlockHeight: 1,
+      lastBlockHeight: 100,
+      lastBlockHeightYet: 100
+    });
+
+    provider = await ProviderSeeder.createInDatabase();
+
+    deployments = await Promise.all([
+      DeploymentSeeder.createInDatabase({
+        owner: provider.owner,
+        dseq: "dseq1",
+        denom: "denom1",
+        createdHeight: 1,
+        closedHeight: null
+      }),
+      DeploymentSeeder.createInDatabase({
+        owner: provider.owner,
+        dseq: "dseq2",
+        denom: "denom1",
+        createdHeight: 2,
+        closedHeight: null
+      }),
+      DeploymentSeeder.createInDatabase({
+        owner: provider.owner,
+        dseq: "dseq3",
+        denom: "denom1",
+        createdHeight: 3,
+        closedHeight: 4
+      })
+    ]);
+
+    const deploymentGroups = await Promise.all([
+      DeploymentGroupSeeder.createInDatabase({
+        deploymentId: deployments[0].id
+      }),
+      DeploymentGroupSeeder.createInDatabase({
+        deploymentId: deployments[1].id
+      }),
+      DeploymentGroupSeeder.createInDatabase({
+        deploymentId: deployments[2].id
+      })
+    ]);
+
+    await Promise.all([
+      LeaseSeeder.createInDatabase({
+        providerAddress: provider.owner,
+        createdHeight: 1,
+        closedHeight: null,
+        deploymentId: deployments[0].id,
+        deploymentGroupId: deploymentGroups[0].id,
+        dseq: deployments[0].dseq,
+        state: "active"
+      }),
+      LeaseSeeder.createInDatabase({
+        providerAddress: provider.owner,
+        createdHeight: 2,
+        closedHeight: null,
+        deploymentId: deployments[1].id,
+        deploymentGroupId: deploymentGroups[1].id,
+        dseq: deployments[1].dseq,
+        state: "active"
+      }),
+      LeaseSeeder.createInDatabase({
+        providerAddress: provider.owner,
+        createdHeight: 3,
+        closedHeight: 4,
+        deploymentId: deployments[2].id,
+        deploymentGroupId: deploymentGroups[2].id,
+        dseq: deployments[2].dseq,
+        state: "closed"
+      })
+    ]);
+
+    await Promise.all([
+      BlockSeeder.createInDatabase({
+        height: 1,
+        timestamp: day.date
+      }),
+      BlockSeeder.createInDatabase({
+        height: 2,
+        timestamp: day.date
+      }),
+      BlockSeeder.createInDatabase({
+        height: 3,
+        timestamp: day.date
+      }),
+      BlockSeeder.createInDatabase({
+        height: 4,
+        timestamp: day.date
+      })
+    ]);
+  });
+
+  afterAll(async () => {
+    await closeConnections();
+    mcache.clear();
+  });
+
+  afterEach(() => {
+    nock.cleanAll();
+  });
+
+  const expectDeployments = (deploymentsFound: Deployment[], deploymentsExpected: Deployment[]) => {
+    expect(deploymentsFound.length).toBe(deploymentsExpected.length);
+
+    const dseqsFound = map(deploymentsFound, "dseq");
+    deploymentsExpected.forEach(deployment => {
+      expect(dseqsFound).toContain(deployment.dseq);
+    });
+  };
+
+  describe("GET /v1/providers/:providerAddress/deployments", () => {
+    it("returns deployments", async () => {
+      const response = await app.request(`/v1/providers/${provider.owner}/deployments/0/2`);
+
+      const data = await response.json();
+
+      expect(response.status).toBe(200);
+      expect(data.total).toBe(3);
+      expectDeployments(data.deployments, [deployments[2], deployments[1]]);
+    });
+
+    it("returns deployments, paginated", async () => {
+      const response = await app.request(`/v1/providers/${provider.owner}/deployments/1/2`);
+
+      const data = await response.json();
+
+      expect(response.status).toBe(200);
+      expect(data.total).toBe(3);
+      expectDeployments(data.deployments, [deployments[1], deployments[0]]);
+    });
+
+    it("respects status filter", async () => {
+      const response = await app.request(`/v1/providers/${provider.owner}/deployments/0/2?status=closed`);
+
+      const data = await response.json();
+
+      expect(response.status).toBe(200);
+      expect(data.total).toBe(1);
+      expectDeployments(data.deployments, [deployments[2]]);
+    });
+  });
+});


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts (1)</summary><blockquote>

`72-77`: **Consider optimizing resource aggregation.**

The current implementation uses multiple `reduce` calls, iterating over the leases array 5 times. This could be optimized into a single iteration.


Consider refactoring to a single loop:

```diff
-      resources: {
-        cpu: d.leases.reduce((acc, l) => acc + l.cpuUnits, 0),
-        memory: d.leases.reduce((acc, l) => acc + l.memoryQuantity, 0),
-        gpu: d.leases.reduce((acc, l) => acc + l.gpuUnits, 0),
-        ephemeralStorage: d.leases.reduce((acc, l) => acc + l.ephemeralStorageQuantity, 0),
-        persistentStorage: d.leases.reduce((acc, l) => acc + l.persistentStorageQuantity, 0)
-      },
+      resources: d.leases.reduce(
+        (acc, l) => ({
+          cpu: acc.cpu + l.cpuUnits,
+          memory: acc.memory + l.memoryQuantity,
+          gpu: acc.gpu + l.gpuUnits,
+          ephemeralStorage: acc.ephemeralStorage + l.ephemeralStorageQuantity,
+          persistentStorage: acc.persistentStorage + l.persistentStorageQuantity
+        }),
+        { cpu: 0, memory: 0, gpu: 0, ephemeralStorage: 0, persistentStorage: 0 }
+      ),
```

</blockquote></details>
<details>
<summary>apps/api/src/provider/http-schemas/provider-deployments.schema.ts (2)</summary><blockquote>

`43-43`: **Address TODO comments for status field validation.**

The status fields have TODO comments but are currently just strings without proper validation. Consider defining proper enum values for deployment and lease statuses.



```diff
+const DeploymentStatusSchema = z.enum(["active", "paused", "closed"]);
+const LeaseStatusSchema = z.enum(["active", "insufficient_funds", "closed"]);
+
 export const ProviderDeploymentsResponseSchema = z.object({
   // ... other fields
-  status: z.string(), //TODO
+  status: DeploymentStatusSchema,
   // ... other fields
   leases: z.array(
     z.object({
       // ... other fields
-      status: z.string(), //TODO
+      status: LeaseStatusSchema,
```

Would you like me to research the actual status values used in the Akash network for more accurate enum definitions?


Also applies to: 64-64

---

`47-53`: **Extract shared resource schema to reduce duplication.**

The resource object structure is duplicated between deployment and lease schemas. Consider extracting it into a shared schema for better maintainability.



```diff
+const ResourceSchema = z.object({
+  cpu: z.number(),
+  memory: z.number(),
+  gpu: z.number(),
+  ephemeralStorage: z.number(),
+  persistentStorage: z.number()
+});
+
 export const ProviderDeploymentsResponseSchema = z.object({
   // ... other fields
-  resources: z.object({
-    cpu: z.number(),
-    memory: z.number(),
-    gpu: z.number(),
-    ephemeralStorage: z.number(),
-    persistentStorage: z.number()
-  }),
+  resources: ResourceSchema,
   leases: z.array(
     z.object({
       // ... other fields
-      resources: z.object({
-        cpu: z.number(),
-        memory: z.number(),
-        gpu: z.number(),
-        ephemeralStorage: z.number(),
-        persistentStorage: z.number()
-      })
+      resources: ResourceSchema
```


Also applies to: 65-71

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b26d8a406a085b165ad1a2b4baf129d62dfe07f0 and 5115651afffd0235a319511ff97286f299a1154c.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `apps/api/src/app.ts` (2 hunks)
* `apps/api/src/provider/controllers/provider-deployments/provider-deployments.controller.ts` (1 hunks)
* `apps/api/src/provider/http-schemas/provider-deployments.schema.ts` (1 hunks)
* `apps/api/src/provider/routes/index.ts` (1 hunks)
* `apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts` (1 hunks)
* `apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts` (1 hunks)
* `apps/api/src/routes/v1/index.ts` (0 hunks)
* `apps/api/src/routes/v1/providers/deployments.ts` (0 hunks)
* `apps/api/src/services/db/deploymentService.ts` (1 hunks)
* `apps/api/test/functional/provider-deployments.spec.ts` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* apps/api/src/routes/v1/index.ts
* apps/api/src/routes/v1/providers/deployments.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>apps/api/src/app.ts (1)</summary><blockquote>

<details>
<summary>apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts (1)</summary>

* `providerDeploymentsRouter` (36-36)

</details>

</blockquote></details>
<details>
<summary>apps/api/src/provider/controllers/provider-deployments/provider-deployments.controller.ts (2)</summary><blockquote>

<details>
<summary>apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts (1)</summary>

* `singleton` (9-98)

</details>
<details>
<summary>apps/api/src/provider/http-schemas/provider-deployments.schema.ts (2)</summary>

* `ProviderDeploymentsQuery` (79-79)
* `ProviderDeploymentsResponse` (80-80)

</details>

</blockquote></details>
<details>
<summary>apps/api/src/provider/http-schemas/provider-deployments.schema.ts (1)</summary><blockquote>

<details>
<summary>apps/api/src/utils/constants.ts (1)</summary>

* `openApiExampleProviderAddress` (15-15)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: test-api-build
* GitHub Check: validate-api

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>apps/api/src/provider/routes/index.ts (1)</summary>

`7-7`: **LGTM! Clean export addition.**

The export follows the established pattern and is logically placed with other provider route exports.

</details>
<details>
<summary>apps/api/src/app.ts (2)</summary>

`50-50`: **LGTM! Consistent import pattern.**

The import follows the established pattern for provider routers and is placed appropriately with related imports.

---

`116-116`: **LGTM! Proper router registration.**

The router is registered consistently with other provider routers in the application.

</details>
<details>
<summary>apps/api/test/functional/provider-deployments.spec.ts (3)</summary>

`20-116`: **Excellent test setup with comprehensive data seeding.**

The test setup creates a realistic scenario with proper relationships between providers, deployments, leases, and blocks. The data seeding covers both active and closed deployments which enables proper testing of status filtering.

---

`127-134`: **Clean helper function for deployment validation.**

The `expectDeployments` helper provides a clear way to validate that the correct deployments are returned by comparing deployment sequence identifiers (dseq).

---

`137-165`: **Comprehensive test coverage for pagination and filtering.**

The tests effectively cover:
- Basic functionality with pagination
- Offset-based pagination behavior  
- Status filtering functionality

The test expectations correctly verify both response structure and content.

</details>
<details>
<summary>apps/api/src/provider/controllers/provider-deployments/provider-deployments.controller.ts (2)</summary>

`6-8`: **Good use of dependency injection pattern.**

The controller properly uses the `@singleton()` decorator and constructor injection for the service dependency, following established patterns in the codebase.

---

`10-25`: **Excellent concurrent query execution and clean response structure.**

The method effectively uses `Promise.all` to execute count and data queries concurrently, which optimizes performance. The response structure is clean and properly typed using the schema definitions.

</details>
<details>
<summary>apps/api/src/services/db/deploymentService.ts (1)</summary>

`1-73`: **Clean refactoring of provider deployment functions.**

The removal of `getProviderDeploymentsCount` and `getProviderDeployments` functions is properly executed, with the functionality successfully moved to the new dedicated `ProviderDeploymentsService`.

</details>
<details>
<summary>apps/api/src/provider/http-schemas/provider-deployments.schema.ts (3)</summary>

`1-5`: **LGTM on imports and constants.**

The imports are appropriate and the maxLimit constant provides good configurability for the pagination limit.

---

`23-32`: **Good query parameter schema design.**

The status filter with enum validation and optional nature is well-designed. The OpenAPI metadata provides clear documentation.

---

`78-80`: **Proper type exports for TypeScript integration.**

The type inference exports follow zod best practices and provide proper TypeScript types for use throughout the codebase.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T19:21:12Z
user: login: jzsfkzm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T19:29:35Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T19:30:01Z
user: login: jzsfkzm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T19:34:21Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T19:34:42Z
user: login: jzsfkzm
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T19:35:08Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T19:37:02Z
user: login: baktun14
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T02:07:58Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T02:08:03Z
user: login: baktun14
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T02:08:09Z

Review comments:
diff_hunk: @@ -0,0 +1,44 @@
+import { createRoute } from "@hono/zod-openapi";
+import { container } from "tsyringe";
+
+import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler";
+import { ProviderDeploymentsController } from "@src/provider/controllers/provider-deployments/provider-deployments.controller";
+import {
+  ProviderDeploymentsParamsSchema,
+  ProviderDeploymentsQuerySchema,
+  ProviderDeploymentsResponseSchema
+} from "@src/provider/http-schemas/provider-deployments.schema";
+
+const route = createRoute({
+  method: "get",
+  path: "/v1/providers/{provider}/deployments/{skip}/{limit}",
+  summary: "Get a list of deployments for a provider.",
+  tags: ["Providers", "Deployments"],
+  request: {
+    params: ProviderDeploymentsParamsSchema,
+    query: ProviderDeploymentsQuerySchema
+  },
+  responses: {
+    200: {
+      description: "Returns deployment list",
+      content: {
+        "application/json": {
+          schema: ProviderDeploymentsResponseSchema
+        }
+      }
+    },
+    400: {
+      description: "Invalid status filter"
+    }
+  }
+});
+
+export const providerDeploymentsRouter = new OpenApiHonoHandler();
+
+providerDeploymentsRouter.openapi(route, async function routeProviderDeployments(c) {
+  const { provider, skip, limit } = c.req.valid("param");
+  const { status } = c.req.valid("query");
+
+  const response = await container.resolve(ProviderDeploymentsController).getProviderDeployments(provider, skip, limit, status);
+  return c.json(response);
+});
path: apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling to the route handler.**

The handler should catch and properly handle errors from the controller to provide meaningful error responses to clients.


Apply this diff to add error handling:

```diff
 providerDeploymentsRouter.openapi(route, async function routeProviderDeployments(c) {
   const { provider, skip, limit } = c.req.valid("param");
   const { status } = c.req.valid("query");
 
-  const response = await container.resolve(ProviderDeploymentsController).getProviderDeployments(provider, skip, limit, status);
-  return c.json(response);
+  try {
+    const response = await container.resolve(ProviderDeploymentsController).getProviderDeployments(provider, skip, limit, status);
+    return c.json(response);
+  } catch (error) {
+    console.error("Error fetching provider deployments:", error);
+    return c.json({ error: "Failed to fetch deployments" }, 500);
+  }
 });
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
providerDeploymentsRouter.openapi(route, async function routeProviderDeployments(c) {
  const { provider, skip, limit } = c.req.valid("param");
  const { status } = c.req.valid("query");

  try {
    const response = await container.resolve(ProviderDeploymentsController)
      .getProviderDeployments(provider, skip, limit, status);
    return c.json(response);
  } catch (error) {
    console.error("Error fetching provider deployments:", error);
    return c.json({ error: "Failed to fetch deployments" }, 500);
  }
});
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts
around lines 38 to 44, the route handler lacks error handling for potential
exceptions from the controller call. Wrap the async function body in a try-catch
block, returning a meaningful error response (e.g., with an appropriate status
code and message) in the catch block to ensure clients receive clear error
information instead of unhandled exceptions.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T19:21:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,44 @@
+import { createRoute } from "@hono/zod-openapi";
+import { container } from "tsyringe";
+
+import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler";
+import { ProviderDeploymentsController } from "@src/provider/controllers/provider-deployments/provider-deployments.controller";
+import {
+  ProviderDeploymentsParamsSchema,
+  ProviderDeploymentsQuerySchema,
+  ProviderDeploymentsResponseSchema
+} from "@src/provider/http-schemas/provider-deployments.schema";
+
+const route = createRoute({
+  method: "get",
+  path: "/v1/providers/{provider}/deployments/{skip}/{limit}",
+  summary: "Get a list of deployments for a provider.",
+  tags: ["Providers", "Deployments"],
+  request: {
+    params: ProviderDeploymentsParamsSchema,
+    query: ProviderDeploymentsQuerySchema
+  },
+  responses: {
+    200: {
+      description: "Returns deployment list",
+      content: {
+        "application/json": {
+          schema: ProviderDeploymentsResponseSchema
+        }
+      }
+    },
+    400: {
+      description: "Invalid status filter"
+    }
+  }
+});
path: apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider moving pagination parameters to query parameters for better API design.**

Using path parameters for pagination (`{skip}/{limit}`) is unconventional and creates less flexible URLs. Standard REST practices recommend using query parameters for pagination.




Consider refactoring the endpoint to:
```diff
- path: "/v1/providers/{provider}/deployments/{skip}/{limit}",
+ path: "/v1/providers/{provider}/deployments",
```

And update the schemas to include skip and limit in the query parameters instead. This would result in cleaner URLs like `/v1/providers/{provider}/deployments?skip=0&limit=10&status=active`.


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts
lines 12 to 34, the pagination parameters skip and limit are currently defined
as path parameters, which is unconventional. Refactor the route to remove skip
and limit from the path and instead include them as query parameters in
ProviderDeploymentsQuerySchema. Update the route path to
"/v1/providers/{provider}/deployments" and adjust the query schema to accept
skip and limit for pagination, resulting in cleaner URLs like
/v1/providers/{provider}/deployments?skip=0&limit=10&status=active.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T19:21:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+import { Block } from "@akashnetwork/database/dbSchemas";
+import { Deployment, Lease } from "@akashnetwork/database/dbSchemas/akash";
+import type { WhereOptions } from "sequelize";
+import { Op } from "sequelize";
+import { singleton } from "tsyringe";
+
+import { ProviderDeploymentsQuery } from "@src/provider/http-schemas/provider-deployments.schema";
+
+@singleton()
+export class ProviderDeploymentsService {
+  async getProviderDeploymentsCount(provider: string, status?: ProviderDeploymentsQuery["status"]) {
+    const leaseFilter: WhereOptions<Lease> = { providerAddress: provider };
+    if (status) {
+      leaseFilter.closedHeight = status === "active" ? null : { [Op.ne]: null };
+    }
+
+    const result = await Deployment.count({
+      include: [{ model: Lease, attributes: [], required: true, where: leaseFilter }]
+    });
+
+    return result;
+  }
+
+  async getProviderDeployments(provider: string, skip: number, limit: number, status?: ProviderDeploymentsQuery["status"]) {
+    const leaseFilter: WhereOptions<Lease> = { providerAddress: provider };
+
+    if (status) {
+      leaseFilter.closedHeight = status === "active" ? null : { [Op.ne]: null };
+    }
+
+    const deploymentDseqs = await Deployment.findAll({
+      attributes: ["dseq", "createdHeight"],
+      include: [{ model: Lease, attributes: [], required: true, where: leaseFilter }],
+      order: [["createdHeight", "DESC"]],
+      offset: skip,
+      limit: limit
+    });
+
+    const deployments = await Deployment.findAll({
+      where: {
+        dseq: { [Op.in]: deploymentDseqs.map(d => d.dseq) }
+      },
+      include: [
+        {
+          model: Lease,
+          required: true,
+          where: { providerAddress: provider },
+          include: [
+            { model: Block, required: true, as: "createdBlock" },
+            { model: Block, required: false, as: "closedBlock" }
+          ]
+        },
+        { model: Block, required: true, as: "createdBlock" },
+        { model: Block, required: false, as: "closedBlock" }
+      ],
+      order: [["createdHeight", "DESC"]]
+    });
+
+    return deployments.map(d => ({
+      owner: d.owner,
+      dseq: d.dseq,
+      denom: d.denom,
+      createdHeight: d.createdHeight,
+      createdDate: d.createdBlock.datetime,
+      closedHeight: d.closedHeight,
+      closedDate: d.closedHeight ? d.closedBlock.datetime : null,
path: apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Add null safety check for closedBlock access.**

The code assumes `closedBlock` exists when `closedHeight` is not null, but `closedBlock` is marked as `required: false` and could be null due to data inconsistencies.


Apply this diff to add null safety:

```diff
-      closedDate: d.closedHeight ? d.closedBlock.datetime : null,
+      closedDate: d.closedHeight && d.closedBlock ? d.closedBlock.datetime : null,
```

And similarly for leases:

```diff
-        closedDate: l.closedHeight ? l.closedBlock.datetime : null,
+        closedDate: l.closedHeight && l.closedBlock ? l.closedBlock.datetime : null,
```


Also applies to: 86-86

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts
at lines 66 and 86, the code accesses closedBlock.datetime assuming closedBlock
is always present when closedHeight is not null, but closedBlock can be null. To
fix this, add a null safety check by verifying closedBlock is not null before
accessing its datetime property, returning null if closedBlock is null. Apply
the same null safety check for leases at line 86.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit d46e358
created_at: 2025-05-25T19:21:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+import { Block } from "@akashnetwork/database/dbSchemas";
+import { Deployment, Lease } from "@akashnetwork/database/dbSchemas/akash";
+import type { WhereOptions } from "sequelize";
+import { Op } from "sequelize";
+import { singleton } from "tsyringe";
+
+import { ProviderDeploymentsQuery } from "@src/provider/http-schemas/provider-deployments.schema";
+
+@singleton()
+export class ProviderDeploymentsService {
+  async getProviderDeploymentsCount(provider: string, status?: ProviderDeploymentsQuery["status"]) {
+    const leaseFilter: WhereOptions<Lease> = { providerAddress: provider };
+    if (status) {
+      leaseFilter.closedHeight = status === "active" ? null : { [Op.ne]: null };
+    }
+
+    const result = await Deployment.count({
+      include: [{ model: Lease, attributes: [], required: true, where: leaseFilter }]
+    });
+
+    return result;
+  }
+
+  async getProviderDeployments(provider: string, skip: number, limit: number, status?: ProviderDeploymentsQuery["status"]) {
+    const leaseFilter: WhereOptions<Lease> = { providerAddress: provider };
+
+    if (status) {
+      leaseFilter.closedHeight = status === "active" ? null : { [Op.ne]: null };
+    }
+
+    const deploymentDseqs = await Deployment.findAll({
+      attributes: ["dseq", "createdHeight"],
+      include: [{ model: Lease, attributes: [], required: true, where: leaseFilter }],
+      order: [["createdHeight", "DESC"]],
+      offset: skip,
+      limit: limit
+    });
+
+    const deployments = await Deployment.findAll({
+      where: {
+        dseq: { [Op.in]: deploymentDseqs.map(d => d.dseq) }
+      },
+      include: [
+        {
+          model: Lease,
+          required: true,
+          where: { providerAddress: provider },
+          include: [
+            { model: Block, required: true, as: "createdBlock" },
+            { model: Block, required: false, as: "closedBlock" }
+          ]
+        },
+        { model: Block, required: true, as: "createdBlock" },
+        { model: Block, required: false, as: "closedBlock" }
+      ],
+      order: [["createdHeight", "DESC"]]
+    });
path: apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Potential ordering inconsistency between pagination and data queries.**

The two-query approach might not preserve the exact order when multiple deployments have the same `createdHeight`. The second query re-orders by `createdHeight DESC` but doesn't guarantee the same sub-ordering within identical heights.


Consider adding a secondary sort key (e.g., `dseq`) to ensure consistent ordering:

```diff
-      order: [["createdHeight", "DESC"]],
+      order: [["createdHeight", "DESC"], ["dseq", "DESC"]],
```

Apply this change to both queries (lines 34 and 56).


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    const deploymentDseqs = await Deployment.findAll({
      attributes: ["dseq", "createdHeight"],
      include: [{ model: Lease, attributes: [], required: true, where: leaseFilter }],
-      order: [["createdHeight", "DESC"]],
+      order: [["createdHeight", "DESC"], ["dseq", "DESC"]],
      offset: skip,
      limit: limit
    });

    const deployments = await Deployment.findAll({
      where: {
        dseq: { [Op.in]: deploymentDseqs.map(d => d.dseq) }
      },
      include: [
        {
          model: Lease,
          required: true,
          where: { providerAddress: provider },
          include: [
            { model: Block, required: true, as: "createdBlock" },
            { model: Block, required: false, as: "closedBlock" }
          ]
        },
        { model: Block, required: true, as: "createdBlock" },
        { model: Block, required: false, as: "closedBlock" }
      ],
-      order: [["createdHeight", "DESC"]]
+      order: [["createdHeight", "DESC"], ["dseq", "DESC"]]
    });
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In
apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts
around lines 31 to 57, both queries order results only by createdHeight
descending, which can cause inconsistent ordering when multiple deployments
share the same createdHeight. To fix this, add a secondary sort key such as dseq
descending to the order array in both queries to ensure stable and consistent
ordering across pagination and data retrieval.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T19:21:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,80 @@
+import { z } from "zod";
+
+import { openApiExampleProviderAddress } from "@src/utils/constants";
+
+const maxLimit = 100;
+
+export const ProviderDeploymentsParamsSchema = z.object({
+  provider: z.string().openapi({
+    description: "Provider Address",
+    example: openApiExampleProviderAddress
+  }),
+  skip: z.coerce.number().min(0).openapi({
+    description: "Deployments to skip",
+    example: 10
+  }),
+  limit: z.coerce.number().min(1).max(maxLimit).openapi({
+    description: "Deployments to return",
+    example: 10,
+    maximum: maxLimit
+  })
+});
path: apps/api/src/provider/http-schemas/provider-deployments.schema.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider moving pagination parameters to query schema.**

The `skip` and `limit` parameters are typically query parameters rather than path parameters for REST APIs. Based on the route `/v1/providers/{address}/deployments`, only `provider` should be a path parameter.



Apply this diff to move pagination parameters to the query schema:

```diff
 export const ProviderDeploymentsParamsSchema = z.object({
   provider: z.string().openapi({
     description: "Provider Address",
     example: openApiExampleProviderAddress
-  }),
-  skip: z.coerce.number().min(0).openapi({
-    description: "Deployments to skip",
-    example: 10
-  }),
-  limit: z.coerce.number().min(1).max(maxLimit).openapi({
-    description: "Deployments to return",
-    example: 10,
-    maximum: maxLimit
   })
 });
```

And update the query schema:

```diff
 export const ProviderDeploymentsQuerySchema = z.object({
+  skip: z.coerce.number().min(0).optional().default(0).openapi({
+    description: "Deployments to skip",
+    example: 10
+  }),
+  limit: z.coerce.number().min(1).max(maxLimit).optional().default(10).openapi({
+    description: "Deployments to return",
+    example: 10,
+    maximum: maxLimit
+  }),
   status: z
     .enum(["active", "closed"])
     .optional()
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In apps/api/src/provider/http-schemas/provider-deployments.schema.ts around
lines 7 to 21, the pagination parameters `skip` and `limit` are incorrectly
defined as part of the path parameters schema. Move `skip` and `limit` out of
the `ProviderDeploymentsParamsSchema` and into a separate query parameters
schema to reflect that they should be query parameters, while keeping `provider`
as the only path parameter. Update the route handling and OpenAPI documentation
accordingly to separate path and query parameters.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-25T19:21:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,44 @@
+import { createRoute } from "@hono/zod-openapi";
+import { container } from "tsyringe";
+
+import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler";
+import { ProviderDeploymentsController } from "@src/provider/controllers/provider-deployments/provider-deployments.controller";
+import {
+  ProviderDeploymentsParamsSchema,
+  ProviderDeploymentsQuerySchema,
+  ProviderDeploymentsResponseSchema
+} from "@src/provider/http-schemas/provider-deployments.schema";
+
+const route = createRoute({
+  method: "get",
+  path: "/v1/providers/{provider}/deployments/{skip}/{limit}",
+  summary: "Get a list of deployments for a provider.",
+  tags: ["Providers", "Deployments"],
+  request: {
+    params: ProviderDeploymentsParamsSchema,
+    query: ProviderDeploymentsQuerySchema
+  },
+  responses: {
+    200: {
+      description: "Returns deployment list",
+      content: {
+        "application/json": {
+          schema: ProviderDeploymentsResponseSchema
+        }
+      }
+    },
+    400: {
+      description: "Invalid status filter"
+    }
+  }
+});
path: apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts
user: login: jzsfkzm
body: Current URL work with params, I think we should keep that for now, until further changes.
created_at: 2025-05-25T19:29:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,44 @@
+import { createRoute } from "@hono/zod-openapi";
+import { container } from "tsyringe";
+
+import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler";
+import { ProviderDeploymentsController } from "@src/provider/controllers/provider-deployments/provider-deployments.controller";
+import {
+  ProviderDeploymentsParamsSchema,
+  ProviderDeploymentsQuerySchema,
+  ProviderDeploymentsResponseSchema
+} from "@src/provider/http-schemas/provider-deployments.schema";
+
+const route = createRoute({
+  method: "get",
+  path: "/v1/providers/{provider}/deployments/{skip}/{limit}",
+  summary: "Get a list of deployments for a provider.",
+  tags: ["Providers", "Deployments"],
+  request: {
+    params: ProviderDeploymentsParamsSchema,
+    query: ProviderDeploymentsQuerySchema
+  },
+  responses: {
+    200: {
+      description: "Returns deployment list",
+      content: {
+        "application/json": {
+          schema: ProviderDeploymentsResponseSchema
+        }
+      }
+    },
+    400: {
+      description: "Invalid status filter"
+    }
+  }
+});
path: apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts
user: login: coderabbitai[bot]
body: @jzsfkzm Understood! You're right that the current path parameter approach is working fine. We can keep the existing implementation as-is and consider the query parameter refactor in future iterations when it makes more sense to address it.


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@jzsfkzm Understood! You're right that the current path parameter approach is working fine. We can keep the existing implementation as-is and consider the query parameter refactor in future iterations when it makes more sense to address it.</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T19:30:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+import { Block } from "@akashnetwork/database/dbSchemas";
+import { Deployment, Lease } from "@akashnetwork/database/dbSchemas/akash";
+import type { WhereOptions } from "sequelize";
+import { Op } from "sequelize";
+import { singleton } from "tsyringe";
+
+import { ProviderDeploymentsQuery } from "@src/provider/http-schemas/provider-deployments.schema";
+
+@singleton()
+export class ProviderDeploymentsService {
+  async getProviderDeploymentsCount(provider: string, status?: ProviderDeploymentsQuery["status"]) {
+    const leaseFilter: WhereOptions<Lease> = { providerAddress: provider };
+    if (status) {
+      leaseFilter.closedHeight = status === "active" ? null : { [Op.ne]: null };
+    }
+
+    const result = await Deployment.count({
+      include: [{ model: Lease, attributes: [], required: true, where: leaseFilter }]
+    });
+
+    return result;
+  }
+
+  async getProviderDeployments(provider: string, skip: number, limit: number, status?: ProviderDeploymentsQuery["status"]) {
+    const leaseFilter: WhereOptions<Lease> = { providerAddress: provider };
+
+    if (status) {
+      leaseFilter.closedHeight = status === "active" ? null : { [Op.ne]: null };
+    }
+
+    const deploymentDseqs = await Deployment.findAll({
+      attributes: ["dseq", "createdHeight"],
+      include: [{ model: Lease, attributes: [], required: true, where: leaseFilter }],
+      order: [["createdHeight", "DESC"]],
+      offset: skip,
+      limit: limit
+    });
+
+    const deployments = await Deployment.findAll({
+      where: {
+        dseq: { [Op.in]: deploymentDseqs.map(d => d.dseq) }
+      },
+      include: [
+        {
+          model: Lease,
+          required: true,
+          where: { providerAddress: provider },
+          include: [
+            { model: Block, required: true, as: "createdBlock" },
+            { model: Block, required: false, as: "closedBlock" }
+          ]
+        },
+        { model: Block, required: true, as: "createdBlock" },
+        { model: Block, required: false, as: "closedBlock" }
+      ],
+      order: [["createdHeight", "DESC"]]
+    });
path: apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts
user: login: jzsfkzm
body: This seems valid, fixed it.
created_at: 2025-05-25T19:34:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+import { Block } from "@akashnetwork/database/dbSchemas";
+import { Deployment, Lease } from "@akashnetwork/database/dbSchemas/akash";
+import type { WhereOptions } from "sequelize";
+import { Op } from "sequelize";
+import { singleton } from "tsyringe";
+
+import { ProviderDeploymentsQuery } from "@src/provider/http-schemas/provider-deployments.schema";
+
+@singleton()
+export class ProviderDeploymentsService {
+  async getProviderDeploymentsCount(provider: string, status?: ProviderDeploymentsQuery["status"]) {
+    const leaseFilter: WhereOptions<Lease> = { providerAddress: provider };
+    if (status) {
+      leaseFilter.closedHeight = status === "active" ? null : { [Op.ne]: null };
+    }
+
+    const result = await Deployment.count({
+      include: [{ model: Lease, attributes: [], required: true, where: leaseFilter }]
+    });
+
+    return result;
+  }
+
+  async getProviderDeployments(provider: string, skip: number, limit: number, status?: ProviderDeploymentsQuery["status"]) {
+    const leaseFilter: WhereOptions<Lease> = { providerAddress: provider };
+
+    if (status) {
+      leaseFilter.closedHeight = status === "active" ? null : { [Op.ne]: null };
+    }
+
+    const deploymentDseqs = await Deployment.findAll({
+      attributes: ["dseq", "createdHeight"],
+      include: [{ model: Lease, attributes: [], required: true, where: leaseFilter }],
+      order: [["createdHeight", "DESC"]],
+      offset: skip,
+      limit: limit
+    });
+
+    const deployments = await Deployment.findAll({
+      where: {
+        dseq: { [Op.in]: deploymentDseqs.map(d => d.dseq) }
+      },
+      include: [
+        {
+          model: Lease,
+          required: true,
+          where: { providerAddress: provider },
+          include: [
+            { model: Block, required: true, as: "createdBlock" },
+            { model: Block, required: false, as: "closedBlock" }
+          ]
+        },
+        { model: Block, required: true, as: "createdBlock" },
+        { model: Block, required: false, as: "closedBlock" }
+      ],
+      order: [["createdHeight", "DESC"]]
+    });
path: apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts
user: login: coderabbitai[bot]
body: @jzsfkzm Thank you for addressing the ordering inconsistency! Adding the secondary sort key will ensure stable pagination results even when deployments share the same creation height.



    (\   /)
   ( ._. )
  o_(")(")

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20pagination%20bugs!%20%F0%9F%90%9B%20Add%20a%20secondary%20sort%20key%20like%20dseq%20alongside%20createdHeight%20DESC%20to%20keep%20consistent%20ordering%20when%20timestamps%20tie.%20Try%20%40coderabbitai%20for%20smart%20code%20fixes!%20%F0%9F%9A%80&url=https%3A%2F%2Fgithub.com%2Fakash-network%2Fconsole%2Fpull%2F1364%23discussion_r2106285184)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@jzsfkzm Thank you for addressing the ordering inconsistency! Adding the secondary sort key will ensure stable pagination results even when deployments share the same creation height.



    (\   /)
   ( ._. )
  o_(")(")

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20pagination%20bugs!%20%F0%9F%90%9B%20Add%20a%20secondary%20sort%20key%20like%20dseq%20alongside%20createdHeight%20DESC%20to%20keep%20consistent%20ordering%20when%20timestamps%20tie.%20Try%20%40coderabbitai%20for%20smart%20code%20fixes!%20%F0%9F%9A%80&url=https%3A%2F%2Fgithub.com%2Fakash-network%2Fconsole%2Fpull%2F1364%23discussion_r2106285184)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T19:34:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,44 @@
+import { createRoute } from "@hono/zod-openapi";
+import { container } from "tsyringe";
+
+import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler";
+import { ProviderDeploymentsController } from "@src/provider/controllers/provider-deployments/provider-deployments.controller";
+import {
+  ProviderDeploymentsParamsSchema,
+  ProviderDeploymentsQuerySchema,
+  ProviderDeploymentsResponseSchema
+} from "@src/provider/http-schemas/provider-deployments.schema";
+
+const route = createRoute({
+  method: "get",
+  path: "/v1/providers/{provider}/deployments/{skip}/{limit}",
+  summary: "Get a list of deployments for a provider.",
+  tags: ["Providers", "Deployments"],
+  request: {
+    params: ProviderDeploymentsParamsSchema,
+    query: ProviderDeploymentsQuerySchema
+  },
+  responses: {
+    200: {
+      description: "Returns deployment list",
+      content: {
+        "application/json": {
+          schema: ProviderDeploymentsResponseSchema
+        }
+      }
+    },
+    400: {
+      description: "Invalid status filter"
+    }
+  }
+});
+
+export const providerDeploymentsRouter = new OpenApiHonoHandler();
+
+providerDeploymentsRouter.openapi(route, async function routeProviderDeployments(c) {
+  const { provider, skip, limit } = c.req.valid("param");
+  const { status } = c.req.valid("query");
+
+  const response = await container.resolve(ProviderDeploymentsController).getProviderDeployments(provider, skip, limit, status);
+  return c.json(response);
+});
path: apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts
user: login: jzsfkzm
body: We don't do this in other routers either, I wouldn't add it, error logging kicks in anyway when it's an error, isn't it?
created_at: 2025-05-25T19:35:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,44 @@
+import { createRoute } from "@hono/zod-openapi";
+import { container } from "tsyringe";
+
+import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler";
+import { ProviderDeploymentsController } from "@src/provider/controllers/provider-deployments/provider-deployments.controller";
+import {
+  ProviderDeploymentsParamsSchema,
+  ProviderDeploymentsQuerySchema,
+  ProviderDeploymentsResponseSchema
+} from "@src/provider/http-schemas/provider-deployments.schema";
+
+const route = createRoute({
+  method: "get",
+  path: "/v1/providers/{provider}/deployments/{skip}/{limit}",
+  summary: "Get a list of deployments for a provider.",
+  tags: ["Providers", "Deployments"],
+  request: {
+    params: ProviderDeploymentsParamsSchema,
+    query: ProviderDeploymentsQuerySchema
+  },
+  responses: {
+    200: {
+      description: "Returns deployment list",
+      content: {
+        "application/json": {
+          schema: ProviderDeploymentsResponseSchema
+        }
+      }
+    },
+    400: {
+      description: "Invalid status filter"
+    }
+  }
+});
+
+export const providerDeploymentsRouter = new OpenApiHonoHandler();
+
+providerDeploymentsRouter.openapi(route, async function routeProviderDeployments(c) {
+  const { provider, skip, limit } = c.req.valid("param");
+  const { status } = c.req.valid("query");
+
+  const response = await container.resolve(ProviderDeploymentsController).getProviderDeployments(provider, skip, limit, status);
+  return c.json(response);
+});
path: apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Find other router files in the provider module to check their error handling patterns
fd -t f -e ts "router" apps/api/src/provider/routes/

# Get content of a few other router files to see how they handle errors
echo "=== Checking provider-dashboard router ==="
cat apps/api/src/provider/routes/provider-dashboard/provider-dashboard.router.ts 2>/dev/null || echo "File not found"

echo "=== Checking provider-regions router ==="
cat apps/api/src/provider/routes/provider-regions/provider-regions.router.ts 2>/dev/null || echo "File not found"

echo "=== Checking provider-versions router ==="
cat apps/api/src/provider/routes/provider-versions/provider-versions.router.ts 2>/dev/null || echo "File not found"
```

Length of output: 4499

---

🏁 Script executed:
```
#!/bin/bash
# Also check some routers outside the provider module to understand the general pattern
echo "=== Looking for routers in other parts of the API ==="
fd -t f -e ts "router" apps/api/src/ | head -10

# Check a router from another module
echo "=== Checking a router from v1 routes ==="
ls apps/api/src/routes/v1/ | head -5

# Look for any global error handling middleware
echo "=== Searching for error handling middleware ==="
rg -A 5 -B 5 "error.*handler|catch.*error" apps/api/src/
```

Length of output: 35470

---



</details>

@jzsfkzm You're absolutely right! I checked other routers in the codebase and confirmed that none of them use explicit try-catch blocks. They all follow the same pattern of directly calling controllers and returning responses.

Looking at the `OpenApiHonoHandler` class, I can see it has a `defaultHook` that automatically handles errors through the `HonoErrorHandlerService`. This means error handling happens centrally at the framework level, making explicit try-catch blocks in individual routes unnecessary and inconsistent with the established architecture.

Thanks for the correction - the current implementation is consistent with the rest of the codebase's error handling approach.

---

<details>
<summary>🧬 Code Graph Analysis Results</summary>

### Relevant snippets for `apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts`

---

#### From `apps/api/src/core/services/open-api-hono-handler/open-api-hono-handler.ts` (lines 11-26):

```typescript
export class OpenApiHonoHandler<E extends AppEnv = AppEnv, S extends Schema = Record<string, never>, BasePath extends string = "/"> extends OpenAPIHono<
  E,
  S,
  BasePath
> {
  constructor(init?: Omit<HonoInit<E>, "defaultHook">) {
    super({
      ...init,
      defaultHook: (result, c) => {
        if (!result.success && "error" in result) {
          return container.resolve(HonoErrorHandlerService).handle(result.error, c);
        }
      }
    });
  }
}
```

**Summary:**
- This class extends `OpenAPIHono` and sets a `defaultHook` in its constructor.
- The `defaultHook` automatically handles errors by invoking `HonoErrorHandlerService.handle` when the result is not successful and contains an error.
- This implies that explicit error logging or handling inside individual route handlers may be unnecessary because errors are centrally handled here.

---

#### From `apps/api/src/provider/http-schemas/provider-deployments.schema.ts` (lines 7-21, 23-32, 34-76):

```typescript
export const ProviderDeploymentsParamsSchema = z.object({
  provider: z.string().openapi({
    description: "Provider Address",
    example: openApiExampleProviderAddress
  }),
  skip: z.coerce.number().min(0).openapi({
    description: "Deployments to skip",
    example: 10
  }),
  limit: z.coerce.number().min(1).max(maxLimit).openapi({
    description: "Deployments to return",
    example: 10,
    maximum: maxLimit
  })
});

export const ProviderDeploymentsQuerySchema = z.object({
  status: z
    .enum(["active", "closed"])
    .optional()
    .openapi({
      description: "Filter by status",
      enum: ["active", "closed"],
      example: "closed"
    })
});

export const ProviderDeploymentsResponseSchema = z.object({
  total: z.number(),
  deployments: z.array(
    z.object({
      owner: z.string(),
      dseq: z.string(),
      denom: z.string(),
      createdHeight: z.number(),
      createdDate: z.date().nullable(),
      status: z.string(),
      balance: z.number(),
      transferred: z.number(),
      settledAt: z.number().nullable(),
      resources: z.object({
        cpu: z.number(),
        memory: z.number(),
        gpu: z.number(),
        ephemeralStorage: z.number(),
        persistentStorage: z.number()
      }),
      leases: z.array(
        z.object({
          provider: z.string(),
          gseq: z.number(),
          oseq: z.number(),
          price: z.number(),
          createdHeight: z.number(),
          createdDate: z.date().nullable(),
          closedHeight: z.number().nullable(),
          closedDate: z.date().nullable(),
          status: z.string(),
          resources: z.object({
            cpu: z.number(),
            memory: z.number(),
            gpu: z.number(),
            ephemeralStorage: z.number(),
            persistentStorage: z.number()
          })
        })
      )
    })
  )
});
```

**Summary:**
- These schemas define the request parameters, query parameters, and response structure for the provider deployments route.
- They use `zod` for validation and OpenAPI metadata.
- This is relevant for understanding the route's input validation and output format.

---

[no other relevant code snippets]

</details>



---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: jzsfkzm
PR: akash-network/console#1364
File: apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts:38-44
Timestamp: 2025-05-25T19:37:00.791Z
Learning: The Akash Console API uses centralized error handling through the OpenApiHonoHandler's defaultHook, which automatically processes errors via HonoErrorHandlerService. Individual route handlers do not use explicit try-catch blocks and instead rely on this framework-level error handling mechanism.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Find other router files in the provider module to check their error handling patterns
fd -t f -e ts "router" apps/api/src/provider/routes/

# Get content of a few other router files to see how they handle errors
echo "=== Checking provider-dashboard router ==="
cat apps/api/src/provider/routes/provider-dashboard/provider-dashboard.router.ts 2>/dev/null || echo "File not found"

echo "=== Checking provider-regions router ==="
cat apps/api/src/provider/routes/provider-regions/provider-regions.router.ts 2>/dev/null || echo "File not found"

echo "=== Checking provider-versions router ==="
cat apps/api/src/provider/routes/provider-versions/provider-versions.router.ts 2>/dev/null || echo "File not found"
```

Length of output: 4499

---

🏁 Script executed:
```
#!/bin/bash
# Also check some routers outside the provider module to understand the general pattern
echo "=== Looking for routers in other parts of the API ==="
fd -t f -e ts "router" apps/api/src/ | head -10

# Check a router from another module
echo "=== Checking a router from v1 routes ==="
ls apps/api/src/routes/v1/ | head -5

# Look for any global error handling middleware
echo "=== Searching for error handling middleware ==="
rg -A 5 -B 5 "error.*handler|catch.*error" apps/api/src/
```

Length of output: 35470

---



</details>

@jzsfkzm You're absolutely right! I checked other routers in the codebase and confirmed that none of them use explicit try-catch blocks. They all follow the same pattern of directly calling controllers and returning responses.

Looking at the `OpenApiHonoHandler` class, I can see it has a `defaultHook` that automatically handles errors through the `HonoErrorHandlerService`. This means error handling happens centrally at the framework level, making explicit try-catch blocks in individual routes unnecessary and inconsistent with the established architecture.

Thanks for the correction - the current implementation is consistent with the rest of the codebase's error handling approach.

---

<details>
<summary>🧬 Code Graph Analysis Results</summary>

### Relevant snippets for `apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts`

---

#### From `apps/api/src/core/services/open-api-hono-handler/open-api-hono-handler.ts` (lines 11-26):

```typescript
export class OpenApiHonoHandler<E extends AppEnv = AppEnv, S extends Schema = Record<string, never>, BasePath extends string = "/"> extends OpenAPIHono<
  E,
  S,
  BasePath
> {
  constructor(init?: Omit<HonoInit<E>, "defaultHook">) {
    super({
      ...init,
      defaultHook: (result, c) => {
        if (!result.success && "error" in result) {
          return container.resolve(HonoErrorHandlerService).handle(result.error, c);
        }
      }
    });
  }
}
```

**Summary:**
- This class extends `OpenAPIHono` and sets a `defaultHook` in its constructor.
- The `defaultHook` automatically handles errors by invoking `HonoErrorHandlerService.handle` when the result is not successful and contains an error.
- This implies that explicit error logging or handling inside individual route handlers may be unnecessary because errors are centrally handled here.

---

#### From `apps/api/src/provider/http-schemas/provider-deployments.schema.ts` (lines 7-21, 23-32, 34-76):

```typescript
export const ProviderDeploymentsParamsSchema = z.object({
  provider: z.string().openapi({
    description: "Provider Address",
    example: openApiExampleProviderAddress
  }),
  skip: z.coerce.number().min(0).openapi({
    description: "Deployments to skip",
    example: 10
  }),
  limit: z.coerce.number().min(1).max(maxLimit).openapi({
    description: "Deployments to return",
    example: 10,
    maximum: maxLimit
  })
});

export const ProviderDeploymentsQuerySchema = z.object({
  status: z
    .enum(["active", "closed"])
    .optional()
    .openapi({
      description: "Filter by status",
      enum: ["active", "closed"],
      example: "closed"
    })
});

export const ProviderDeploymentsResponseSchema = z.object({
  total: z.number(),
  deployments: z.array(
    z.object({
      owner: z.string(),
      dseq: z.string(),
      denom: z.string(),
      createdHeight: z.number(),
      createdDate: z.date().nullable(),
      status: z.string(),
      balance: z.number(),
      transferred: z.number(),
      settledAt: z.number().nullable(),
      resources: z.object({
        cpu: z.number(),
        memory: z.number(),
        gpu: z.number(),
        ephemeralStorage: z.number(),
        persistentStorage: z.number()
      }),
      leases: z.array(
        z.object({
          provider: z.string(),
          gseq: z.number(),
          oseq: z.number(),
          price: z.number(),
          createdHeight: z.number(),
          createdDate: z.date().nullable(),
          closedHeight: z.number().nullable(),
          closedDate: z.date().nullable(),
          status: z.string(),
          resources: z.object({
            cpu: z.number(),
            memory: z.number(),
            gpu: z.number(),
            ephemeralStorage: z.number(),
            persistentStorage: z.number()
          })
        })
      )
    })
  )
});
```

**Summary:**
- These schemas define the request parameters, query parameters, and response structure for the provider deployments route.
- They use `zod` for validation and OpenAPI metadata.
- This is relevant for understanding the route's input validation and output format.

---

[no other relevant code snippets]

</details>



---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: jzsfkzm
PR: akash-network/console#1364
File: apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts:38-44
Timestamp: 2025-05-25T19:37:00.791Z
Learning: The Akash Console API uses centralized error handling through the OpenApiHonoHandler's defaultHook, which automatically processes errors via HonoErrorHandlerService. Individual route handlers do not use explicit try-catch blocks and instead rely on this framework-level error handling mechanism.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-25T19:37:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,44 @@
+import { createRoute } from "@hono/zod-openapi";
+import { container } from "tsyringe";
+
+import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler";
+import { ProviderDeploymentsController } from "@src/provider/controllers/provider-deployments/provider-deployments.controller";
+import {
+  ProviderDeploymentsParamsSchema,
+  ProviderDeploymentsQuerySchema,
+  ProviderDeploymentsResponseSchema
+} from "@src/provider/http-schemas/provider-deployments.schema";
+
+const route = createRoute({
+  method: "get",
+  path: "/v1/providers/{provider}/deployments/{skip}/{limit}",
+  summary: "Get a list of deployments for a provider.",
+  tags: ["Providers", "Deployments"],
+  request: {
+    params: ProviderDeploymentsParamsSchema,
+    query: ProviderDeploymentsQuerySchema
+  },
+  responses: {
+    200: {
+      description: "Returns deployment list",
+      content: {
+        "application/json": {
+          schema: ProviderDeploymentsResponseSchema
+        }
+      }
+    },
+    400: {
+      description: "Invalid status filter"
+    }
+  }
+});
path: apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts
user: login: baktun14
body: Yes we don't want to change the structure of the api routes in the current version
created_at: 2025-05-28T02:07:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,44 @@
+import { createRoute } from "@hono/zod-openapi";
+import { container } from "tsyringe";
+
+import { OpenApiHonoHandler } from "@src/core/services/open-api-hono-handler/open-api-hono-handler";
+import { ProviderDeploymentsController } from "@src/provider/controllers/provider-deployments/provider-deployments.controller";
+import {
+  ProviderDeploymentsParamsSchema,
+  ProviderDeploymentsQuerySchema,
+  ProviderDeploymentsResponseSchema
+} from "@src/provider/http-schemas/provider-deployments.schema";
+
+const route = createRoute({
+  method: "get",
+  path: "/v1/providers/{provider}/deployments/{skip}/{limit}",
+  summary: "Get a list of deployments for a provider.",
+  tags: ["Providers", "Deployments"],
+  request: {
+    params: ProviderDeploymentsParamsSchema,
+    query: ProviderDeploymentsQuerySchema
+  },
+  responses: {
+    200: {
+      description: "Returns deployment list",
+      content: {
+        "application/json": {
+          schema: ProviderDeploymentsResponseSchema
+        }
+      }
+    },
+    400: {
+      description: "Invalid status filter"
+    }
+  }
+});
path: apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts
user: login: coderabbitai[bot]
body: > Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

<!-- This is an auto-generated mute comment by CodeRabbit -->
created_at: 2025-05-28T02:08:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-25T19:16:08Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

A new API endpoint for listing provider deployments was introduced, replacing the previous implementation. The update includes new controller, service, schema, and router modules, and removes related legacy route and service code. Functional tests were added to verify the new endpoint's behavior, and route aggregation files were updated accordingly.

## Changes

| File(s)                                                                                       | Change Summary                                                                                                 |
|----------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| apps/api/src/app.ts, apps/api/src/provider/routes/index.ts, apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts | Registered new provider deployments router and exported it from provider routes index.                         |
| apps/api/src/provider/controllers/provider-deployments/provider-deployments.controller.ts     | Added new controller for provider deployments with method to fetch deployments and counts.                     |
| apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts           | Added service for querying provider deployments and counts using ORM.                                          |
| apps/api/src/provider/http-schemas/provider-deployments.schema.ts                             | Introduced zod schemas and types for provider deployments endpoint parameters, queries, and responses.         |
| apps/api/src/routes/v1/index.ts                                                               | Removed import and export of legacy providerDeployments route.                                                 |
| apps/api/src/routes/v1/providers/deployments.ts                                               | Deleted old provider deployments route definition and handler.                                                 |
| apps/api/src/services/db/deploymentService.ts                                                 | Removed legacy functions for provider deployments count and retrieval.                                         |
| apps/api/test/functional/provider-deployments.spec.ts                                         | Added new functional test suite for provider deployments endpoint.                                             |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant Router
    participant Controller
    participant Service
    participant DB

    Client->>Router: GET /v1/providers/{provider}/deployments/{skip}/{limit}?status=
    Router->>Controller: getProviderDeployments(provider, skip, limit, status)
    Controller->>Service: getProviderDeploymentsCount(provider, status)
    Controller->>Service: getProviderDeployments(provider, skip, limit, status)
    Service->>DB: Query deployments and leases with filters
    Service-->>Controller: Return deployments and count
    Controller-->>Router: { total, deployments }
    Router-->>Client: JSON response
```

## Suggested reviewers

- baktun14  
- stalniy

## Poem

> In the warren of code where the data flows deep,  
> A new route emerges for deployments to keep.  
> Controllers and services, schemas anew,  
> With tests to ensure all the numbers are true.  
> The old is now gone, the new hops along—  
> Provider deployments, robust and strong! 🐇✨

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
> 
> npm warn config production Use `--omit=dev` instead.
> npm error Exit handler never called!
> npm error This is an error with npm itself. Please report this error at:
> npm error   <https://github.com/npm/cli/issues>
> npm error A complete log of this run can be found in: /.npm/_logs/2025-05-26T22_46_14_151Z-debug-0.log
> 
> 
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ea37072fbca3d509920ce54c40631b2a1ffd5fa5 and 823578c07f9127517c46ad51254066af55e77be2.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `apps/api/src/app.ts` (2 hunks)
* `apps/api/src/provider/controllers/provider-deployments/provider-deployments.controller.ts` (1 hunks)
* `apps/api/src/provider/http-schemas/provider-deployments.schema.ts` (1 hunks)
* `apps/api/src/provider/routes/index.ts` (1 hunks)
* `apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts` (1 hunks)
* `apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts` (1 hunks)
* `apps/api/src/routes/v1/index.ts` (0 hunks)
* `apps/api/src/routes/v1/providers/deployments.ts` (0 hunks)
* `apps/api/src/services/db/deploymentService.ts` (1 hunks)
* `apps/api/test/functional/provider-deployments.spec.ts` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* apps/api/src/routes/v1/index.ts
* apps/api/src/routes/v1/providers/deployments.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (8)</summary>

* apps/api/src/provider/routes/index.ts
* apps/api/src/provider/controllers/provider-deployments/provider-deployments.controller.ts
* apps/api/test/functional/provider-deployments.spec.ts
* apps/api/src/provider/routes/provider-deployments/provider-deployments.router.ts
* apps/api/src/app.ts
* apps/api/src/services/db/deploymentService.ts
* apps/api/src/provider/http-schemas/provider-deployments.schema.ts
* apps/api/src/provider/services/provider-deployments/provider-deployments.service.ts

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: validate-api
* GitHub Check: test-api-build

</details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=akash-network/console&utm_content=1364):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-25T19:26:16Z
body: ## [Codecov](https://app.codecov.io/gh/akash-network/console/pull/1364?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 35.83%. Comparing base [(`789b00a`)](https://app.codecov.io/gh/akash-network/console/commit/789b00af90300047d2b0aa93400818111453c074?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network) to head [(`823578c`)](https://app.codecov.io/gh/akash-network/console/commit/823578c07f9127517c46ad51254066af55e77be2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network).
> Report is 13 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1364      +/-   ##
==========================================
+ Coverage   35.63%   35.83%   +0.20%     
==========================================
  Files         808      811       +3     
  Lines       19592    19604      +12     
  Branches     3628     3627       -1     
==========================================
+ Hits         6981     7026      +45     
+ Misses      11875    11844      -31     
+ Partials      736      734       -2     
```

| [Flag](https://app.codecov.io/gh/akash-network/console/pull/1364/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network) | Coverage Δ | | *Carryforward flag |
|---|---|---|---|
| [api](https://app.codecov.io/gh/akash-network/console/pull/1364/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network) | `66.29% <100.00%> (+0.64%)` | :arrow_up: | |
| [deploy-web](https://app.codecov.io/gh/akash-network/console/pull/1364/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network) | `14.77% <ø> (ø)` | | Carriedforward from [789b00a](https://app.codecov.io/gh/akash-network/console/commit/789b00af90300047d2b0aa93400818111453c074?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network) |
| [notifications](https://app.codecov.io/gh/akash-network/console/pull/1364/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network) | `88.67% <ø> (ø)` | | Carriedforward from [789b00a](https://app.codecov.io/gh/akash-network/console/commit/789b00af90300047d2b0aa93400818111453c074?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network) |
| [provider-proxy](https://app.codecov.io/gh/akash-network/console/pull/1364/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network) | `80.09% <ø> (ø)` | | Carriedforward from [789b00a](https://app.codecov.io/gh/akash-network/console/commit/789b00af90300047d2b0aa93400818111453c074?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network) |

*This pull request uses carry forward flags. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network) to find out more.

| [Files with missing lines](https://app.codecov.io/gh/akash-network/console/pull/1364?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network) | Coverage Δ | |
|---|---|---|
| [apps/api/src/app.ts](https://app.codecov.io/gh/akash-network/console/pull/1364?src=pr&el=tree&filepath=apps%2Fapi%2Fsrc%2Fapp.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network#diff-YXBwcy9hcGkvc3JjL2FwcC50cw==) | `90.17% <100.00%> (+0.08%)` | :arrow_up: |
| [...der-deployments/provider-deployments.controller.ts](https://app.codecov.io/gh/akash-network/console/pull/1364?src=pr&el=tree&filepath=apps%2Fapi%2Fsrc%2Fprovider%2Fcontrollers%2Fprovider-deployments%2Fprovider-deployments.controller.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network#diff-YXBwcy9hcGkvc3JjL3Byb3ZpZGVyL2NvbnRyb2xsZXJzL3Byb3ZpZGVyLWRlcGxveW1lbnRzL3Byb3ZpZGVyLWRlcGxveW1lbnRzLmNvbnRyb2xsZXIudHM=) | `100.00% <100.00%> (ø)` | |
| [...ovider/http-schemas/provider-deployments.schema.ts](https://app.codecov.io/gh/akash-network/console/pull/1364?src=pr&el=tree&filepath=apps%2Fapi%2Fsrc%2Fprovider%2Fhttp-schemas%2Fprovider-deployments.schema.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network#diff-YXBwcy9hcGkvc3JjL3Byb3ZpZGVyL2h0dHAtc2NoZW1hcy9wcm92aWRlci1kZXBsb3ltZW50cy5zY2hlbWEudHM=) | `100.00% <100.00%> (ø)` | |
| [...rovider-deployments/provider-deployments.router.ts](https://app.codecov.io/gh/akash-network/console/pull/1364?src=pr&el=tree&filepath=apps%2Fapi%2Fsrc%2Fprovider%2Froutes%2Fprovider-deployments%2Fprovider-deployments.router.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network#diff-YXBwcy9hcGkvc3JjL3Byb3ZpZGVyL3JvdXRlcy9wcm92aWRlci1kZXBsb3ltZW50cy9wcm92aWRlci1kZXBsb3ltZW50cy5yb3V0ZXIudHM=) | `100.00% <100.00%> (ø)` | |
| [...ovider-deployments/provider-deployments.service.ts](https://app.codecov.io/gh/akash-network/console/pull/1364?src=pr&el=tree&filepath=apps%2Fapi%2Fsrc%2Fprovider%2Fservices%2Fprovider-deployments%2Fprovider-deployments.service.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network#diff-YXBwcy9hcGkvc3JjL3Byb3ZpZGVyL3NlcnZpY2VzL3Byb3ZpZGVyLWRlcGxveW1lbnRzL3Byb3ZpZGVyLWRlcGxveW1lbnRzLnNlcnZpY2UudHM=) | `100.00% <100.00%> (ø)` | |
| [apps/api/src/services/db/deploymentService.ts](https://app.codecov.io/gh/akash-network/console/pull/1364?src=pr&el=tree&filepath=apps%2Fapi%2Fsrc%2Fservices%2Fdb%2FdeploymentService.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akash-network#diff-YXBwcy9hcGkvc3JjL3NlcnZpY2VzL2RiL2RlcGxveW1lbnRTZXJ2aWNlLnRz) | `23.07% <100.00%> (+7.69%)` | :arrow_up: |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE