feat(operator): partner management with attach/detach (O.6)
- Partners list with name/domain/status/customers/margin + Create modal - Partner detail: contract card, contact card, customers table, attach modal, terminate (soft-delete) danger card - Operator proxies for /partners + /partners/:slug/tenants - platform-api: add partnerId Prop to Tenant schema. The field was being silently dropped by Mongoose because the schema didn't declare it. - tenants.service: rewrite update() to build $set/$unset explicitly and cast partnerId via new Types.ObjectId(). Handles detach via $unset so the field vanishes from the doc cleanly.
This commit is contained in:
@@ -29,6 +29,10 @@ export class Tenant {
|
||||
@Prop({ type: [String], default: [] })
|
||||
domains!: string[]
|
||||
|
||||
// Optional MSP/reseller this tenant belongs to. Sparse — direct tenants have none.
|
||||
@Prop({ type: Types.ObjectId, ref: 'Partner', index: true, sparse: true })
|
||||
partnerId?: Types.ObjectId
|
||||
|
||||
// External system handles — filled in by the provisioning worker (Phase 4)
|
||||
@Prop({ index: true, sparse: true })
|
||||
authentikGroupId?: string
|
||||
|
||||
@@ -62,8 +62,27 @@ export class TenantsService {
|
||||
}
|
||||
|
||||
async update(slug: string, dto: UpdateTenantDto): Promise<TenantDocument> {
|
||||
// Build $set / $unset explicitly. Doing `findOneAndUpdate({slug}, dto, ...)`
|
||||
// with a class-transformer instance leaks undefined slots into the update,
|
||||
// and Mongoose doesn't always cast string→ObjectId for ref fields when
|
||||
// wrapped that way. Explicit $set keeps the intent clear and handles the
|
||||
// detach case (`partnerId: null`) cleanly via $unset.
|
||||
const set: Record<string, unknown> = {}
|
||||
const unset: Record<string, ''> = {}
|
||||
if (dto.name !== undefined) set.name = dto.name
|
||||
if (dto.status !== undefined) set.status = dto.status
|
||||
if (dto.plan !== undefined) set.plan = dto.plan
|
||||
if (dto.domains !== undefined) set.domains = dto.domains
|
||||
if (dto.partnerId !== undefined) {
|
||||
if (dto.partnerId === null) unset.partnerId = ''
|
||||
else set.partnerId = new Types.ObjectId(dto.partnerId)
|
||||
}
|
||||
const update: Record<string, unknown> = {}
|
||||
if (Object.keys(set).length) update.$set = set
|
||||
if (Object.keys(unset).length) update.$unset = unset
|
||||
|
||||
const tenant = await this.tenantModel
|
||||
.findOneAndUpdate({ slug }, dto, { new: true, runValidators: true })
|
||||
.findOneAndUpdate({ slug }, update, { new: true, runValidators: true })
|
||||
.exec()
|
||||
if (!tenant) throw new NotFoundException(`Tenant "${slug}" not found`)
|
||||
return tenant
|
||||
|
||||
Reference in New Issue
Block a user