Merge pull request 'fix(marketplace): ownership :own-KU учитывает trustees председателя КУ' (#16) from chore/trustees-ownership into marketplace2
Reviewed-on: #16
This commit was merged in pull request #16.
This commit is contained in:
+9
@@ -95,6 +95,10 @@ import {
|
||||
} from './services/marketplace-payout-sync.service';
|
||||
import { MarketplaceOutgoingPaymentResolver } from './resolvers/marketplace-outgoing-payment.resolver';
|
||||
import { MarketplaceNotificationService } from './services/marketplace-notification.service';
|
||||
import {
|
||||
MarketplaceBranchOwnershipService,
|
||||
MARKETPLACE_BRANCH_OWNERSHIP_SERVICE,
|
||||
} from './services/marketplace-branch-ownership.service';
|
||||
import {
|
||||
MarketplaceIssuanceService,
|
||||
MARKETPLACE_ISSUANCE_SERVICE,
|
||||
@@ -288,6 +292,11 @@ import { MarketplaceInventoryEntity } from '../infrastructure/entities/marketpla
|
||||
// Слушает per-contract event-bus, отправка через Novu без обратного
|
||||
// влияния на основной flow (INV-12: emit после save в PG).
|
||||
MarketplaceNotificationService,
|
||||
{
|
||||
provide: MARKETPLACE_BRANCH_OWNERSHIP_SERVICE,
|
||||
useClass: MarketplaceBranchOwnershipService,
|
||||
},
|
||||
MarketplaceBranchOwnershipService,
|
||||
// Story 6.1 / 6.3 / 6.4 — выдача пайщику с двойной подписью АПП
|
||||
// (signiss1 + signiss2) и тремя ветками сверки факт vs заказ.
|
||||
{
|
||||
|
||||
+13
-2
@@ -24,6 +24,10 @@ import {
|
||||
MARKETPLACE_APL_RECEPTION_REPOSITORY,
|
||||
type MarketplaceAplReceptionDomainRepository,
|
||||
} from '../../domain/repositories/marketplace-apl-reception.repository';
|
||||
import {
|
||||
MARKETPLACE_BRANCH_OWNERSHIP_SERVICE,
|
||||
MarketplaceBranchOwnershipService,
|
||||
} from '../services/marketplace-branch-ownership.service';
|
||||
import { GeneratedDocumentDTO } from '~/application/document/dto/generated-document.dto';
|
||||
import type { DocumentDomainEntity } from '~/domain/document/entity/document-domain.entity';
|
||||
|
||||
@@ -44,7 +48,9 @@ export class MarketplaceAplReceptionResolver {
|
||||
@Inject(MARKETPLACE_APL_RECEPTION_SERVICE)
|
||||
private readonly service: MarketplaceAplReceptionService,
|
||||
@Inject(MARKETPLACE_APL_RECEPTION_REPOSITORY)
|
||||
private readonly receptionRepo: MarketplaceAplReceptionDomainRepository
|
||||
private readonly receptionRepo: MarketplaceAplReceptionDomainRepository,
|
||||
@Inject(MARKETPLACE_BRANCH_OWNERSHIP_SERVICE)
|
||||
private readonly branchOwnership: MarketplaceBranchOwnershipService
|
||||
) {}
|
||||
|
||||
@Mutation(() => MarketplaceAplReceptionResultDTO, {
|
||||
@@ -155,9 +161,14 @@ export class MarketplaceAplReceptionResolver {
|
||||
@UseGuards(GqlJwtAuthGuard, MarketplaceMembershipGuard, MarketplaceRoleGuard)
|
||||
@RequireMarketplaceAccess('Receiving', 'create')
|
||||
async marketplaceListAplReceptionsByBraname(
|
||||
@CurrentMarketplaceMember() _member: IMarketplaceCurrentMember,
|
||||
@CurrentMarketplaceMember() member: IMarketplaceCurrentMember,
|
||||
@Args('data') data: MarketplaceListAplReceptionsByBranameInputDTO
|
||||
): Promise<MarketplaceAplReceptionDTO[]> {
|
||||
await this.branchOwnership.assertCanActAsBraname(
|
||||
config.coopname,
|
||||
member.username,
|
||||
data.braname
|
||||
);
|
||||
const list = await this.receptionRepo.listByBraname(config.coopname, data.braname);
|
||||
return list.map(toMarketplaceAplReceptionDTO);
|
||||
}
|
||||
|
||||
+37
-7
@@ -1,12 +1,13 @@
|
||||
import { Inject, Injectable, UseGuards } from '@nestjs/common';
|
||||
import { ForbiddenException, Inject, Injectable, UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import config from '~/config/config';
|
||||
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
|
||||
import { CurrentMarketplaceMember } from '../decorators/current-marketplace-member.decorator';
|
||||
import { RequireMarketplaceAccess } from '../decorators/marketplace-access.decorator';
|
||||
import { RequireMarketplaceRole } from '../decorators/marketplace-role.decorator';
|
||||
import { MarketplaceMembershipGuard } from '../guards/marketplace-membership.guard';
|
||||
import { MarketplaceRoleGuard } from '../guards/marketplace-role.guard';
|
||||
import type { IMarketplaceCurrentMember } from '../dto/marketplace-current-member.dto';
|
||||
import type { MarketplaceRole } from '../membership/marketplace-roles.mapper';
|
||||
import {
|
||||
MarketplaceInventoryItemDTO,
|
||||
MarketplaceLabelInventoryInputDTO,
|
||||
@@ -25,6 +26,11 @@ import {
|
||||
type MarketplaceInventoryDomainRepository,
|
||||
type MarketplaceInventoryListFilter,
|
||||
} from '../../domain/repositories/marketplace-inventory.repository';
|
||||
import {
|
||||
MARKETPLACE_BRANCH_OWNERSHIP_SERVICE,
|
||||
MarketplaceBranchOwnershipService,
|
||||
} from '../services/marketplace-branch-ownership.service';
|
||||
import { RequireMarketplaceAccess } from '../decorators/marketplace-access.decorator';
|
||||
import type {
|
||||
MarketplaceBarcodeFormat,
|
||||
MarketplaceBarcodeStrategy,
|
||||
@@ -38,7 +44,9 @@ export class MarketplaceInventoryResolver {
|
||||
@Inject(MARKETPLACE_INVENTORY_LABEL_SERVICE)
|
||||
private readonly labelService: MarketplaceInventoryLabelService,
|
||||
@Inject(MARKETPLACE_INVENTORY_REPOSITORY)
|
||||
private readonly inventoryRepo: MarketplaceInventoryDomainRepository
|
||||
private readonly inventoryRepo: MarketplaceInventoryDomainRepository,
|
||||
@Inject(MARKETPLACE_BRANCH_OWNERSHIP_SERVICE)
|
||||
private readonly branchOwnership: MarketplaceBranchOwnershipService
|
||||
) {}
|
||||
|
||||
@Mutation(() => MarketplaceLabelInventoryResultDTO, {
|
||||
@@ -97,19 +105,41 @@ export class MarketplaceInventoryResolver {
|
||||
|
||||
@Query(() => [MarketplaceInventoryItemDTO], {
|
||||
name: 'marketplaceListInventory',
|
||||
description: 'Список наклеек инвентаря КУ — для admin-стола склада и операторских разделов.',
|
||||
description:
|
||||
'Список наклеек инвентаря КУ: admin/совет видят весь склад кооператива, оператор — только свой участок.',
|
||||
})
|
||||
@UseGuards(GqlJwtAuthGuard, MarketplaceMembershipGuard, MarketplaceRoleGuard)
|
||||
@RequireMarketplaceAccess('Warehouse', 'read:own-KU')
|
||||
@RequireMarketplaceRole('admin', 'board_readonly', 'operator')
|
||||
async marketplaceListInventory(
|
||||
@CurrentMarketplaceMember() _member: IMarketplaceCurrentMember,
|
||||
@CurrentMarketplaceMember() member: IMarketplaceCurrentMember,
|
||||
@Args('data', { nullable: true }) data?: MarketplaceListInventoryInputDTO
|
||||
): Promise<MarketplaceInventoryItemDTO[]> {
|
||||
const roles = member.marketplace_roles as MarketplaceRole[];
|
||||
const isAdmin = roles.includes('admin') || roles.includes('board_readonly');
|
||||
const isOperator = roles.includes('operator');
|
||||
|
||||
let branameFilter = data?.braname;
|
||||
if (!isAdmin) {
|
||||
if (!isOperator) {
|
||||
throw new ForbiddenException('Нет доступа к складу.');
|
||||
}
|
||||
if (!branameFilter) {
|
||||
throw new ForbiddenException(
|
||||
'Оператор обязан указать кооперативный участок для чтения склада.'
|
||||
);
|
||||
}
|
||||
await this.branchOwnership.assertCanActAsBraname(
|
||||
config.coopname,
|
||||
member.username,
|
||||
branameFilter
|
||||
);
|
||||
}
|
||||
|
||||
const filter: MarketplaceInventoryListFilter = {
|
||||
coopname: config.coopname,
|
||||
order_id: data?.order_id,
|
||||
shipment_id: data?.shipment_id,
|
||||
braname: data?.braname,
|
||||
braname: branameFilter,
|
||||
status: data?.statuses?.length
|
||||
? (data.statuses as MarketplaceInventoryStatus[])
|
||||
: undefined,
|
||||
|
||||
+13
-1
@@ -23,6 +23,10 @@ import {
|
||||
MARKETPLACE_ORDER_REPOSITORY,
|
||||
type MarketplaceOrderDomainRepository,
|
||||
} from '../../domain/repositories/marketplace-order.repository';
|
||||
import {
|
||||
MARKETPLACE_BRANCH_OWNERSHIP_SERVICE,
|
||||
MarketplaceBranchOwnershipService,
|
||||
} from '../services/marketplace-branch-ownership.service';
|
||||
import { GeneratedDocumentDTO } from '~/application/document/dto/generated-document.dto';
|
||||
import type { DocumentDomainEntity } from '~/domain/document/entity/document-domain.entity';
|
||||
|
||||
@@ -43,7 +47,9 @@ export class MarketplaceIssuanceResolver {
|
||||
@Inject(MARKETPLACE_ISSUANCE_SERVICE)
|
||||
private readonly service: MarketplaceIssuanceService,
|
||||
@Inject(MARKETPLACE_ORDER_REPOSITORY)
|
||||
private readonly orderRepo: MarketplaceOrderDomainRepository
|
||||
private readonly orderRepo: MarketplaceOrderDomainRepository,
|
||||
@Inject(MARKETPLACE_BRANCH_OWNERSHIP_SERVICE)
|
||||
private readonly branchOwnership: MarketplaceBranchOwnershipService
|
||||
) {}
|
||||
|
||||
@Mutation(() => MarketplaceIssuanceResultDTO, {
|
||||
@@ -139,8 +145,14 @@ export class MarketplaceIssuanceResolver {
|
||||
@UseGuards(GqlJwtAuthGuard, MarketplaceMembershipGuard, MarketplaceRoleGuard)
|
||||
@RequireMarketplaceAccess('Issuance', 'read:own-KU')
|
||||
async marketplaceListIssuancesByBraname(
|
||||
@CurrentMarketplaceMember() member: IMarketplaceCurrentMember,
|
||||
@Args('data') data: MarketplaceListIssuancesByBranameInputDTO
|
||||
): Promise<MarketplaceOrderDTO[]> {
|
||||
await this.branchOwnership.assertCanActAsBraname(
|
||||
config.coopname,
|
||||
member.username,
|
||||
data.delivery_braname
|
||||
);
|
||||
const orders = await this.orderRepo.listForIssuanceByBraname(
|
||||
config.coopname,
|
||||
data.delivery_braname
|
||||
|
||||
+42
-18
@@ -1,4 +1,4 @@
|
||||
import { ForbiddenException, Injectable, UseGuards } from '@nestjs/common';
|
||||
import { ForbiddenException, Inject, Injectable, UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import config from '~/config/config';
|
||||
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
|
||||
@@ -22,6 +22,10 @@ import {
|
||||
} from '../dto/marketplace-return-claim.dto';
|
||||
import { MarketplaceReturnClaimService } from '../services/marketplace-return-claim.service';
|
||||
import { toMarketplaceReturnClaimDTO } from './marketplace-return-claim.mapper';
|
||||
import {
|
||||
MARKETPLACE_BRANCH_OWNERSHIP_SERVICE,
|
||||
MarketplaceBranchOwnershipService,
|
||||
} from '../services/marketplace-branch-ownership.service';
|
||||
|
||||
function toGeneratedDocumentDTO(e: DocumentDomainEntity): GeneratedDocumentDTO {
|
||||
const dto = new GeneratedDocumentDTO();
|
||||
@@ -46,7 +50,11 @@ function toGeneratedDocumentDTO(e: DocumentDomainEntity): GeneratedDocumentDTO {
|
||||
@Resolver()
|
||||
@Injectable()
|
||||
export class MarketplaceReturnClaimResolver {
|
||||
constructor(private readonly service: MarketplaceReturnClaimService) {}
|
||||
constructor(
|
||||
private readonly service: MarketplaceReturnClaimService,
|
||||
@Inject(MARKETPLACE_BRANCH_OWNERSHIP_SERVICE)
|
||||
private readonly branchOwnership: MarketplaceBranchOwnershipService
|
||||
) {}
|
||||
|
||||
@Query(() => GeneratedDocumentDTO, {
|
||||
name: 'marketplaceReturnClaimSignablePayload',
|
||||
@@ -105,6 +113,11 @@ export class MarketplaceReturnClaimResolver {
|
||||
@CurrentMarketplaceMember() member: IMarketplaceCurrentMember,
|
||||
@Args('data') data: MarketplaceApproveReturnVisitInputDTO
|
||||
): Promise<MarketplaceReturnClaimResultDTO> {
|
||||
await this.branchOwnership.assertCanActAsBraname(
|
||||
config.coopname,
|
||||
member.username,
|
||||
data.braname
|
||||
);
|
||||
const result = await this.service.approveReturnVisit({
|
||||
coopname: config.coopname,
|
||||
chairman_account: member.username,
|
||||
@@ -127,6 +140,11 @@ export class MarketplaceReturnClaimResolver {
|
||||
@CurrentMarketplaceMember() member: IMarketplaceCurrentMember,
|
||||
@Args('data') data: MarketplaceRejectReturnRemoteInputDTO
|
||||
): Promise<MarketplaceReturnClaimResultDTO> {
|
||||
await this.branchOwnership.assertCanActAsBraname(
|
||||
config.coopname,
|
||||
member.username,
|
||||
data.braname
|
||||
);
|
||||
const result = await this.service.rejectReturnRemote({
|
||||
coopname: config.coopname,
|
||||
chairman_account: member.username,
|
||||
@@ -149,6 +167,11 @@ export class MarketplaceReturnClaimResolver {
|
||||
@CurrentMarketplaceMember() member: IMarketplaceCurrentMember,
|
||||
@Args('data') data: MarketplaceAcceptReturnAtVisitInputDTO
|
||||
): Promise<MarketplaceReturnClaimResultDTO> {
|
||||
await this.branchOwnership.assertCanActAsBraname(
|
||||
config.coopname,
|
||||
member.username,
|
||||
data.braname
|
||||
);
|
||||
const result = await this.service.acceptReturnAtVisit({
|
||||
coopname: config.coopname,
|
||||
chairman_account: member.username,
|
||||
@@ -173,6 +196,11 @@ export class MarketplaceReturnClaimResolver {
|
||||
@CurrentMarketplaceMember() member: IMarketplaceCurrentMember,
|
||||
@Args('data') data: MarketplaceRejectReturnAtVisitInputDTO
|
||||
): Promise<MarketplaceReturnClaimResultDTO> {
|
||||
await this.branchOwnership.assertCanActAsBraname(
|
||||
config.coopname,
|
||||
member.username,
|
||||
data.braname
|
||||
);
|
||||
const result = await this.service.rejectReturnAtVisit({
|
||||
coopname: config.coopname,
|
||||
chairman_account: member.username,
|
||||
@@ -209,15 +237,11 @@ export class MarketplaceReturnClaimResolver {
|
||||
@CurrentMarketplaceMember() member: IMarketplaceCurrentMember,
|
||||
@Args('data') data: MarketplaceListReturnClaimsByBranameInputDTO
|
||||
): Promise<MarketplaceReturnClaimDTO[]> {
|
||||
// Ownership `:own-KU` — MVP-приближение: член-operator является
|
||||
// chairman'ом braname если его username совпадает с account КУ
|
||||
// (соответствует текущему fallback'у marketplace-notification.service.ts).
|
||||
// Полная интеграция с `coop_ku.chairman_account` — Phase 2.
|
||||
if (member.username !== data.delivery_braname) {
|
||||
throw new ForbiddenException(
|
||||
'Чтение заявлений возможно только для участка, председателем которого вы являетесь.'
|
||||
);
|
||||
}
|
||||
await this.branchOwnership.assertCanActAsBraname(
|
||||
config.coopname,
|
||||
member.username,
|
||||
data.delivery_braname
|
||||
);
|
||||
const claims = await this.service.listByDeliveryBraname(
|
||||
config.coopname,
|
||||
data.delivery_braname
|
||||
@@ -236,13 +260,13 @@ export class MarketplaceReturnClaimResolver {
|
||||
@Args('claim_id') claim_id: string
|
||||
): Promise<MarketplaceReturnClaimDTO> {
|
||||
const claim = await this.service.findById(config.coopname, claim_id);
|
||||
// Ownership-проверка `:own` — matrix capability проверена guard'ом,
|
||||
// здесь верифицируем что пайщик действительно владелец заявления
|
||||
// (orderer Order'а либо председатель КУ доставки этого заявления).
|
||||
const isOwnerOrderer = claim.orderer_account === member.username;
|
||||
const isChairmanOfDeliveryKu = member.marketplace_roles.includes('operator')
|
||||
&& claim.delivery_braname === member.username;
|
||||
if (!isOwnerOrderer && !isChairmanOfDeliveryKu) {
|
||||
const isOwner = claim.orderer_account === member.username;
|
||||
const isChairmanOrTrustee = await this.branchOwnership.canActAsBraname(
|
||||
config.coopname,
|
||||
member.username,
|
||||
claim.delivery_braname
|
||||
);
|
||||
if (!isOwner && !isChairmanOrTrustee) {
|
||||
throw new ForbiddenException('Это чужое заявление на возврат.');
|
||||
}
|
||||
return this.toClaimDTO(claim);
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
BRANCH_BLOCKCHAIN_PORT,
|
||||
type BranchBlockchainPort,
|
||||
} from '~/domain/branch/interfaces/branch-blockchain.port';
|
||||
|
||||
export const MARKETPLACE_BRANCH_OWNERSHIP_SERVICE = Symbol(
|
||||
'MarketplaceBranchOwnershipService'
|
||||
);
|
||||
|
||||
/**
|
||||
* Проверяет, что аккаунт правомочен оперировать от имени кооперативного участка.
|
||||
*
|
||||
* Правомочны: председатель КУ (`branch.trustee`) либо его доверенные лица
|
||||
* (`branch.trusted[]`, до 3 individual-аккаунтов; см. `branch::addtrusted`).
|
||||
*
|
||||
* Branches не реплицируются в Postgres — chain RPC через port. ADR-011
|
||||
* не нарушается: read-path вычисляет authorization, а не отдаёт прикладную
|
||||
* сущность пользователю.
|
||||
*/
|
||||
@Injectable()
|
||||
export class MarketplaceBranchOwnershipService {
|
||||
constructor(
|
||||
@Inject(BRANCH_BLOCKCHAIN_PORT)
|
||||
private readonly branchPort: BranchBlockchainPort
|
||||
) {}
|
||||
|
||||
async canActAsBraname(coopname: string, account: string, braname: string): Promise<boolean> {
|
||||
const branch = await this.branchPort.getBranch(coopname, braname);
|
||||
if (!branch) return false;
|
||||
if (branch.trustee === account) return true;
|
||||
return branch.trusted?.includes(account) ?? false;
|
||||
}
|
||||
|
||||
async assertCanActAsBraname(
|
||||
coopname: string,
|
||||
account: string,
|
||||
braname: string
|
||||
): Promise<void> {
|
||||
const branch = await this.branchPort.getBranch(coopname, braname);
|
||||
if (!branch) {
|
||||
throw new NotFoundException(`Кооперативный участок ${braname} не найден.`);
|
||||
}
|
||||
const allowed =
|
||||
branch.trustee === account || (branch.trusted?.includes(account) ?? false);
|
||||
if (!allowed) {
|
||||
throw new ForbiddenException(
|
||||
'Действие доступно только председателю кооперативного участка или его доверенному лицу.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user