Ir al contenido

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.

Catálogo de TCGs habilitados (Pokémon, One Piece, etc.).

CampoTipoNullableDefaultDescripción
_idStringnoSlug del TCG (pokemon, one-piece, etc.)
slugStringnoSlug (mismo valor que _id) — único
nameStringnoNombre display
descriptionStringno''Descripción corta
enabledBooleannotrueSi está habilitado
sourceStringno'tcgplayer' u otro
externalMixedno{}Metadata de la fuente externa

Sin índices explícitos (unique en slug).

Sets de cada TCG (e.g. “Scarlet & Violet 151”).

CampoTipoNullableDefaultDescripción
_idStringno{tcg}-{slug}
tcgStringnoSlug del TCG (índice)
slugStringnoSlug del set
nameStringnoNombre
codeStringCódigo corto
releaseDateDateFecha lanzamiento
cardsCountNumberno0Cuántas cartas tiene

Índices: {tcg:1, slug:1} (unique), {tcg:1, releaseDate:-1}.

Cartas individuales sincronizadas desde TCGplayer.

CampoTipoNullableDefaultDescripción
_idStringno{tcg}-{externalProductId}
tcgStringnoSlug TCG
setIdStringnoRef al set
setSlug / setNameStringnoDenormalizado para queries
nameStringnoNombre completo (puede incluir variante)
baseNameStringnoNombre canónico para autocomplete
numberStringNúmero en el set
rarityStringRareza
imageUrlStringnoURL del catálogo (TCGplayer/CDN)
marketPricesSub-docPrecios 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}.

Reportes de sincronización con TCGplayer (auditoría de jobs).

Índices: {source:1}, {tcg:1}.

Publicaciones de los vendedores. Discriminated union por kind.

Campos comunes a todos los kinds:

CampoTipoNullableDescripción
_idStringnolisting-{nanoid}
sellerIdStringnoRef al user
kindenum card/accessory/bulk/sealednoTipo de producto
priceNumbernoPrecio en CLP cents (mín 1)
currencyenum CLPnoSolo CLP por ahora
quantityNumbernoStock disponible (mín 1)
photos[String]noHasta N fotos (Cloudinary URLs)
customPhotoStringFoto custom legacy
descriptionStringHasta 500 chars
statusenumnoactive / paused / sold / hidden_by_admin / hidden_by_user_suspension
locationSub-docno{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.

CampoTipoDescripción
_idStringuserId
sellerIdStringnullable; un cart solo puede tener items de un seller
itemsarray{listingId, quantity}

Las órdenes generadas en checkout.

CampoTipoDescripción
_idStringorder-{nanoid} — interno, en URLs
displayIdString8 chars + dash — visible para el usuario
buyerId, sellerIdStringrefs
itemsarraysnapshot de items al momento del checkout
totalPriceNumbersuma de subtotales en CLP cents
buyerAddressSub-docdirección snapshot al checkout
statusenumawaiting_payment/pending/accepted/shipped/completed/canceled
deliveryMethodenumshipping/in_person/null
paymentId, paymentTimeoutAt, mpInitPointrefs a payments
openDisputeId, warningEmailSentAttracking
acceptedAt/shippedAt/completedAt/canceledAtDatetimestamps

Í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).

Tabla de pagos contra MP. Un payment por order.

Índices: {userId:1, createdAt:-1}, {mpPaymentId:1} (unique partial).

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}.

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).

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}.

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}.

Ledger inmutable de movimientos.

Índices: {userId:1, createdAt:-1}, {metadata.orderId:1} (sparse), {metadata.withdrawalId:1} (sparse), {type:1, createdAt:-1}.

Solicitudes de retiro de wallet a cuenta bancaria.

Estados: pending/processing/completed/canceled. Índices: {userId:1, createdAt:-1}, {status:1, createdAt:-1}.

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.

Cuentas de usuario.

CampoTipoDescripción
_idStringuser-{nanoid}
emailStringunique, lowercase
usernameStringunique
name, avatarUrlperfil
authProvidersarraygoogle por ahora
region, commune, street, houseNumber, phonedirección
contactPreferences, notificationPreferencesSub-doctoggles
reviewStatsSub-docrating promedio + count
isAdminBooleanflag admin
statusenumactive/warned/suspended/banned
suspendedUntil, warningsCount, lastWarningAt, warningAcknowledgedAtmoderación
walletBalanceNumberbalance actual (denormalizado del ledger)
isSystemBooleanla cuenta system de la plataforma

Índices: {email:1} (unique), {username:1} (unique), {status:1, suspendedUntil:1} (partial para auto-unsuspend).

Banners visuales para el home.

Campos: imageUrl, imageScale/Offset, CTA primary/secondary, dark variant, status (draft/published/archived), order. Índices: {status:1, order:1}.

Audit log de acciones de admin.

Índices: {adminId:1, createdAt:-1}.

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).