Skip to main content

Categories API

This page describes the web app endpoints that list and create product categories ("Tipo de Producto"), including the ?organizationId= scoping rules for partner and vendor organizations.

Routes: apps/web/server/api/categories/. Controller: modules/inventory/src/categories/infrastructure/controllers/Category.ts. Repository: modules/inventory/src/categories/infrastructure/repositories/PrismaCategory.ts.

Caller classification​

CallerCondition (_resolveOrgScope)
PartnerThe caller organization has no partnerId
VendorThe caller organization has a partnerId

Classification uses organizations.partner_id, not organizations.type.

?organizationId= resolution​

Applies to GET /api/categories and POST /api/categories.

CallerorganizationIdScopeResult
Vendorany value or absentIgnoredOwn organization
Partnerabsent—GET: all vendors. POST: own organization
Partnerown idOwn organizationGET: own categories. POST: own organization
Partnerid of one of its vendorsThat vendorGET: that vendor's categories. POST: stored in that vendor
Partnerany other id—403 La organización no pertenece a este partner

GET /api/categories​

ItemValue
Permissionorg:categories:read
Query searchCase-insensitive substring match on name
Query limitPage size. Default 50 when search is set, otherwise unlimited
Query offsetRows to skip
Query organizationIdSee resolution
Ordercreated_at DESC

Results by scope​

ScopeRepository callRows returned
Vendor, or partner with its own idfind(callerOrgId)Every category visible under RLS with app.organization_id = callerOrgId (no explicit organization filter)
Partner with a vendor idfindByOrgIds(callerOrgId, [vendorId])Categories where organization_id = vendorId
Partner without organizationIdfindByOrgIds(callerOrgId, allVendorIds)Categories of all vendors. [] when the partner has no vendors

Response 200​

Array of categories:

FieldType
idnumber
namestring (lowercase)
descriptionstring or undefined
organizationIdstring
organization{ id, name }
createdAtdate
updatedAtdate or undefined

POST /api/categories​

ItemValue
Permissionorg:categories:create
Query organizationIdSee resolution
Body{ "name": string, "description"?: string }
Stored namename.toLowerCase().trim()
Stored organizationResolved scope (own organization or the selected vendor)

Responses​

StatusBodyCause
201Created category—
400Invalid bodyBody missing or not an object
403Insufficient permissions: org:categories:create requiredMissing permission
403La organización no pertenece a este partnerorganizationId is not the partner or one of its vendors
409Ya existe un tipo de producto con ese nombreA category with that name is visible in the target organization's RLS context, or the insert violates categories_name_organization_id_key
500An unknown server error occurred. (code: <prisma code>)Any other database error

Error mapping in PrismaCategory​

Raw queries surface Postgres errors as Prisma P2010.

Prisma codeSQLSTATEConstraint in detailResult
P201023505categories_name_organization_id_key409 Ya existe un tipo de producto con ese nombre
any otheranyanyLogged, then PrismaErrorHandler (500 for P2010)

SQLSTATE is read from meta.code, or parsed from the message text (Code: \23505``). Log line format:

[PrismaCategory.<operation>] <prisma code> sqlState=<sqlstate or n/a>: <detail>

POST /api/categories/resolve​

ItemValue
Permissionorg:categories:read
Body{ "names": string[] }
BehaviorLooks up each name and creates missing ones in the caller organization
ResponseRecord<string, number> (original name → category id)
400Se requiere un array de nombres de tipos de producto when names is empty or missing

RLS facts​

FactDetail
Tablecategories has RLS enabled (categories_all_policy)
Visible rowsorganization_id = app.organization_id OR organization_id belongs to a vendor of app.organization_id
BindingEvery repository method runs set_config('app.organization_id', $1, true) and the query in one transaction
findByOrgIdsBinds the caller (partner) organization, then filters by organization_id IN (...)
createBinds the target organization (the vendor when a partner creates for a vendor)
Duplicate checkfindByNames runs in the target organization's RLS context

UI consumer​

apps/web/components/product/form/CategoryAutocomplete.vue sends organizationId when its organization-id prop is set. The edit page passes the product owner organization (product.organization.id). The create page does not pass it.