Modelo de datos (Mongo)
Base de datos en MongoDB Atlas, database api-cards. Driver: Mongoose 8. Cada colección está definida en src/**/persistence/*Schema.ts del repo tcgcards-api.
Colecciones por dominio
Sección titulada «Colecciones por dominio»Catálogo
Sección titulada «Catálogo»Catálogo de TCGs habilitados (Pokémon, One Piece, etc.).
| Campo | Tipo | Nullable | Default | Descripción |
|---|---|---|---|---|
_id | String | no | — | Slug del TCG (pokemon, one-piece, etc.) |
slug | String | no | — | Slug (mismo valor que _id) — único |
name | String | no | — | Nombre display |
description | String | no | '' | Descripción corta |
enabled | Boolean | no | true | Si está habilitado |
source | String | no | — | 'tcgplayer' u otro |
external | Mixed | no | {} | Metadata de la fuente externa |
Sin índices explícitos (unique en slug).
Sets de cada TCG (e.g. “Scarlet & Violet 151”).
| Campo | Tipo | Nullable | Default | Descripción |
|---|---|---|---|---|
_id | String | no | — | {tcg}-{slug} |
tcg | String | no | — | Slug del TCG (índice) |
slug | String | no | — | Slug del set |
name | String | no | — | Nombre |
code | String | sí | — | Código corto |
releaseDate | Date | sí | — | Fecha lanzamiento |
cardsCount | Number | no | 0 | Cuántas cartas tiene |
Índices: {tcg:1, slug:1} (unique), {tcg:1, releaseDate:-1}.
Cartas individuales sincronizadas desde TCGplayer.
| Campo | Tipo | Nullable | Default | Descripción |
|---|---|---|---|---|
_id | String | no | — | {tcg}-{externalProductId} |
tcg | String | no | — | Slug TCG |
setId | String | no | — | Ref al set |
setSlug / setName | String | no | — | Denormalizado para queries |
name | String | no | — | Nombre completo (puede incluir variante) |
baseName | String | no | — | Nombre canónico para autocomplete |
number | String | sí | — | Número en el set |
rarity | String | sí | — | Rareza |
imageUrl | String | no | — | URL del catálogo (TCGplayer/CDN) |
marketPrices | Sub-doc | sí | — | Precios USD del catálogo |
Índices: {tcg:1, setSlug:1}, text index {tcg:1, name:'text'}, {tcg:1, rarity:1}, {tcg:1, baseName:1}, {tcg:1, 'attributes.raw.releaseDate':-1}.
sync_reports
Sección titulada «sync_reports»Reportes de sincronización con TCGplayer (auditoría de jobs).
Índices: {source:1}, {tcg:1}.
Listings y carrito
Sección titulada «Listings y carrito»listings
Sección titulada «listings»Publicaciones de los vendedores. Discriminated union por kind.
Campos comunes a todos los kinds:
| Campo | Tipo | Nullable | Descripción |
|---|---|---|---|
_id | String | no | listing-{nanoid} |
sellerId | String | no | Ref al user |
kind | enum card/accessory/bulk/sealed | no | Tipo de producto |
price | Number | no | Precio en CLP cents (mín 1) |
currency | enum CLP | no | Solo CLP por ahora |
quantity | Number | no | Stock disponible (mín 1) |
photos | [String] | no | Hasta N fotos (Cloudinary URLs) |
customPhoto | String | sí | Foto custom legacy |
description | String | sí | Hasta 500 chars |
status | enum | no | active / paused / sold / hidden_by_admin / hidden_by_user_suspension |
location | Sub-doc | no | {region, commune} |
Campos por kind:
card:cardId,condition,language,variant,hasLeagueStamp,customTitle(legacy).accessory:accessoryType,accessoryCondition,accessoryName,tcg.bulk:bulkComposition,bulkCondition,bulkLanguages([]),cardsPerLot,bulkTitle.sealed:sealedTitle,sealedLanguage.
Índices: {cardId:1, status:1}, {sellerId:1, status:1}, {status:1, createdAt:-1}, {cardId:1, condition:1, language:1}, {sellerId:1, createdAt:-1} (seller_createdAt_idx).
Un cart por usuario. _id = userId.
| Campo | Tipo | Descripción |
|---|---|---|
_id | String | userId |
sellerId | String | nullable; un cart solo puede tener items de un seller |
items | array | {listingId, quantity} |
Órdenes y pagos
Sección titulada «Órdenes y pagos»Las órdenes generadas en checkout.
| Campo | Tipo | Descripción |
|---|---|---|
_id | String | order-{nanoid} — interno, en URLs |
displayId | String | 8 chars + dash — visible para el usuario |
buyerId, sellerId | String | refs |
items | array | snapshot de items al momento del checkout |
totalPrice | Number | suma de subtotales en CLP cents |
buyerAddress | Sub-doc | dirección snapshot al checkout |
status | enum | awaiting_payment/pending/accepted/shipped/completed/canceled |
deliveryMethod | enum | shipping/in_person/null |
paymentId, paymentTimeoutAt, mpInitPoint | — | refs a payments |
openDisputeId, warningEmailSentAt | — | tracking |
acceptedAt/shippedAt/completedAt/canceledAt | Date | timestamps |
Índices: {buyerId:1, createdAt:-1}, {sellerId:1, createdAt:-1}, {status:1, updatedAt:-1}, {displayId:1} (unique sparse), {createdAt:-1, _id:-1}, {status:1, paymentTimeoutAt:1} (← agregado 2026-05-20 para cron de cleanup).
payments
Sección titulada «payments»Tabla de pagos contra MP. Un payment por order.
Índices: {userId:1, createdAt:-1}, {mpPaymentId:1} (unique partial).
conversations + messages
Sección titulada «conversations + messages»Chat entre comprador y vendedor por cada orden.
conversations índices: {orderId:1} (unique), {buyerId:1, lastMessageAt:-1}, {sellerId:1, lastMessageAt:-1}.
messages índices: {conversationId:1, createdAt:1}.
Disputes, reviews, reports
Sección titulada «Disputes, reviews, reports»disputes
Sección titulada «disputes»Una dispute por orden (máximo 1).
Campos clave: reason (not_received/damaged/incorrect/counterfeit/no_response/other), description (min 30 chars), photos (max 4), status (open/resolved), resolution (canceled_in_favor_of_buyer/closed_in_favor_of_seller/dismissed), response (sub-doc).
Índices: {status:1, createdAt:-1}, {orderId:1} (unique).
reviews
Sección titulada «reviews»Reseñas de buyer → seller. Una por orden completada.
Campos: rating (1-5), comment (max 500).
Índices: {orderId:1} (unique), {sellerId:1, createdAt:-1}, {buyerId:1, createdAt:-1}.
reports
Sección titulada «reports»Reportes de usuarios (a listings o a otros users).
Índices: {reporterId:1, targetType:1, targetId:1} (unique partial), {status:1, createdAt:-1}, {targetType:1, targetId:1, status:1}, {reporterId:1, createdAt:-1}.
Wallet, withdrawals, cuentas bancarias
Sección titulada «Wallet, withdrawals, cuentas bancarias»wallet_entries
Sección titulada «wallet_entries»Ledger inmutable de movimientos.
Índices: {userId:1, createdAt:-1}, {metadata.orderId:1} (sparse), {metadata.withdrawalId:1} (sparse), {type:1, createdAt:-1}.
withdrawals
Sección titulada «withdrawals»Solicitudes de retiro de wallet a cuenta bancaria.
Estados: pending/processing/completed/canceled.
Índices: {userId:1, createdAt:-1}, {status:1, createdAt:-1}.
bank_accounts
Sección titulada «bank_accounts»Cuentas bancarias guardadas, encriptadas con AES-GCM.
Campos: userId (unique), ciphertext, iv, authTag, keyVersion, last4, bankName.
La encriptación usa la env var BANK_ACCOUNT_ENCRYPTION_KEY (32 bytes base64). Ver runbooks para rotación.
Usuarios y moderación
Sección titulada «Usuarios y moderación»Cuentas de usuario.
| Campo | Tipo | Descripción |
|---|---|---|
_id | String | user-{nanoid} |
email | String | unique, lowercase |
username | String | unique |
name, avatarUrl | — | perfil |
authProviders | array | google por ahora |
region, commune, street, houseNumber, phone | — | dirección |
contactPreferences, notificationPreferences | Sub-doc | toggles |
reviewStats | Sub-doc | rating promedio + count |
isAdmin | Boolean | flag admin |
status | enum | active/warned/suspended/banned |
suspendedUntil, warningsCount, lastWarningAt, warningAcknowledgedAt | — | moderación |
walletBalance | Number | balance actual (denormalizado del ledger) |
isSystem | Boolean | la cuenta system de la plataforma |
Índices: {email:1} (unique), {username:1} (unique), {status:1, suspendedUntil:1} (partial para auto-unsuspend).
homepageBanners
Sección titulada «homepageBanners»Banners visuales para el home.
Campos: imageUrl, imageScale/Offset, CTA primary/secondary, dark variant, status (draft/published/archived), order.
Índices: {status:1, order:1}.
adminActions
Sección titulada «adminActions»Audit log de acciones de admin.
Índices: {adminId:1, createdAt:-1}.
Resumen
Sección titulada «Resumen»21 colecciones en total. Las más voluminosas a futuro: cards (catálogo), listings, messages. Las más críticas para integridad: orders, payments, wallet_entries (ledger).