Merge pull request 'Эпик 1 ревью: guards membership + MEDIUM/LOW' (#1) from chore/review-E1-onboarding-fixes into marketplace2

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-05-20 12:01:36 +00:00
7 changed files with 46 additions and 21 deletions
@@ -1,4 +1,4 @@
import { Inject, Injectable, NotFoundException, Optional } from '@nestjs/common';
import { BadRequestException, Inject, Injectable, NotFoundException, Optional } from '@nestjs/common';
import {
EXTENSION_REPOSITORY,
@@ -96,7 +96,21 @@ export class MarketplaceCoopAcceptanceService {
);
}
const accepted_at = input.accepted_at ?? new Date().toISOString();
const now = new Date();
const accepted_at = input.accepted_at ?? now.toISOString();
if (input.accepted_at) {
const parsed = new Date(input.accepted_at);
if (Number.isNaN(parsed.getTime())) {
throw new BadRequestException(
`Некорректный формат accepted_at: '${input.accepted_at}' — ожидается ISO-8601`
);
}
if (parsed.getTime() > now.getTime() + 60_000) {
throw new BadRequestException(
`accepted_at не может быть в будущем (${input.accepted_at})`
);
}
}
const acceptance: ICoopAcceptanceConfig = {
accepted: true,
document_registry_id: input.document_registry_id,
@@ -18,7 +18,7 @@ import {
* создан в document factory — Story 1.7 не выполнена) — port не вызывается,
* SignUp не предлагает оферту marketplace; функция возвращает false;
* • при реальном `registry_id > 0` — `registerAgreement` × 1 для оферты ЦПП
* «Стол заказов» с типами аккаунтов `individual` + `entrepreneur`.
* «Стол заказов» с типами аккаунтов `individual` + `entrepreneur` + `organization`.
*
* Идемпотентность гарантируется самим `AgreementRegistryService` (повторный
* register с тем же id+extension_name перезаписывает запись, см. док-комментарий
@@ -41,7 +41,11 @@ export function registerMarketplaceInAgreementRegistry(
title: 'Оферта по целевой потребительской программе «Стол заказов»',
checkbox_text: 'Я прочитал и принимаю',
link_text: 'оферту по целевой потребительской программе «Стол заказов»',
applicable_account_types: [AccountType.individual, AccountType.entrepreneur],
applicable_account_types: [
AccountType.individual,
AccountType.entrepreneur,
AccountType.organization,
],
order: 7,
extension_name: MARKETPLACE_EXTENSION_NAME,
});
@@ -1,8 +1,7 @@
import { Resolver, Query, Args } from '@nestjs/graphql';
import { Injectable, UseGuards } from '@nestjs/common';
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
import { RolesGuard } from '~/application/auth/guards/roles.guard';
import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
import { MarketplaceMembershipGuard } from '../guards/marketplace-membership.guard';
import { AttributeDomainService } from '../../domain/services/attribute-domain.service';
import { AttributeDTO, AttributeGroupDTO, AttributeStatsDTO, DictionaryValueDTO } from '../dto/attribute.dto';
import { SearchAttributesInput } from '../dto/search-attributes-input.dto';
@@ -31,9 +30,13 @@ export class AttributeValidationResult {
}
/**
* GraphQL резолвер для атрибутов marketplace
* GraphQL резолвер для атрибутов marketplace.
*
* Доступ — пайщикам кооператива (через `MarketplaceMembershipGuard`).
* Story 1.3 / 1.8: словарь атрибутов — закрытый ресурс marketplace.
*/
@Resolver(() => AttributeDTO)
@UseGuards(GqlJwtAuthGuard, MarketplaceMembershipGuard)
@Injectable()
export class AttributeResolver {
constructor(private readonly attributeService: AttributeDomainService) {}
@@ -203,8 +206,6 @@ export class AttributeResolver {
name: 'marketplaceAttributeStats',
description: 'Получить статистику по атрибутам marketplace',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member'])
async getAttributeStats(): Promise<AttributeStatsDTO> {
const stats = await this.attributeService.getAttributeStats();
return new AttributeStatsDTO(stats);
@@ -1,8 +1,7 @@
import { Resolver, Query, Args, Int } from '@nestjs/graphql';
import { Inject, UseGuards } from '@nestjs/common';
import { GqlJwtAuthGuard } from '~/application/auth/guards/graphql-jwt-auth.guard';
import { RolesGuard } from '~/application/auth/guards/roles.guard';
import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
import { MarketplaceMembershipGuard } from '../guards/marketplace-membership.guard';
import { CategoryTreeService, CATEGORY_TREE_SERVICE } from '../services/category-tree.service';
import { CategoryTreeDomainService, CATEGORY_TREE_DOMAIN_SERVICE } from '../../domain/services/category-tree-domain.service';
import { CategoryDTO, ProductTypeDTO } from '../dto/category-tree.dto';
@@ -13,9 +12,13 @@ import { GetProductTypeByIdInput } from '../dto/get-product-type-by-id-input.dto
import { SearchCategoriesInput } from '../dto/search-categories-input.dto';
/**
* GraphQL резолвер для работы с деревом категорий marketplace
* GraphQL резолвер для работы с деревом категорий marketplace.
*
* Доступ — пайщикам кооператива (через `MarketplaceMembershipGuard`).
* Story 1.3 / 1.8: каталог категорий — закрытый ресурс marketplace, не публичный.
*/
@Resolver(() => CategoryDTO)
@UseGuards(GqlJwtAuthGuard, MarketplaceMembershipGuard)
export class CategoryTreeResolver {
constructor(
@Inject(CATEGORY_TREE_SERVICE)
@@ -82,8 +85,6 @@ export class CategoryTreeResolver {
name: 'marketplaceGetCategoryTreeStats',
description: 'Получить статистику по дереву категорий',
})
@UseGuards(GqlJwtAuthGuard, RolesGuard)
@AuthRoles(['chairman', 'member'])
async getCategoryTreeStats(): Promise<CategoryTreeStatsDTO> {
return this.categoryTreeService.getCategoryTreeStats();
}
@@ -1,6 +1,7 @@
import { Resolver, Mutation, Query, Args, Int } from '@nestjs/graphql';
import { Injectable, Inject, UseGuards } from '@nestjs/common';
import { RequestDomainService, REQUEST_DOMAIN_SERVICE } from '../../domain/services/request-domain.service';
import { MarketplaceMembershipGuard } from '../guards/marketplace-membership.guard';
import { RolesGuard } from '~/application/auth/guards/roles.guard';
import { AuthRoles } from '~/application/auth/decorators/auth.decorator';
import { RequestDTO } from '../dto/request.dto';
@@ -21,9 +22,13 @@ import { CurrentUser } from '~/application/auth/decorators/current-user.decorato
import type { MonoAccountDomainInterface } from '~/domain/account/interfaces/mono-account-domain.interface';
/**
* GraphQL resolver для работы с заявками marketplace
* GraphQL resolver для работы с заявками marketplace.
*
* Доступ — пайщикам кооператива (через `MarketplaceMembershipGuard`).
* Story 1.3 / 1.8: заявки — закрытый ресурс marketplace, не публичный.
*/
@Resolver(() => RequestDTO)
@UseGuards(GqlJwtAuthGuard, MarketplaceMembershipGuard)
@Injectable()
export class RequestResolver {
constructor(
@@ -136,7 +136,7 @@ export class RequestDomainService {
parentHash: params.parentHash,
coopname: params.coopname,
username: params.username,
// braname://TODO: добавить участок поставки / получения
// TODO: добавить участок поставки / получения
type: params.type,
status: RequestStatus.DRAFT,
name: params.name,
@@ -20,7 +20,7 @@ import { registerMarketplaceInAgreementRegistry } from './application/registrati
/**
* Optional-инжектируемый порт файлового хранилища. Имя расширения marketplace
* подключается к bucket через `@coopenomics/inter` (см. AR31 в epics.md).
* До merge PR #359 `feat(file-storage)` адаптера ещё нет — поэтому через
* Адаптер file-storage подключается опционально через DI — поэтому через
* `@Optional` и опциональный токен.
*/
export const MARKETPLACE_FILE_STORAGE_PORT = Symbol('MARKETPLACE_FILE_STORAGE_PORT');
@@ -98,13 +98,13 @@ export class MarketplacePlugin extends BaseExtModule {
/**
* Bucket для хранения изображений Стола заказов и фотографий гарантийного
* возврата. Имя — `coop-<coopname>` (см. AR31). Если адаптер file-storage
* не подключён (PR #359 не вмержен), пропускаем шаг с warn-логом; install
* расширения не падает оставшиеся шаги выполняются.
* отключён конфигурацией — пропускаем шаг с info-логом; install расширения
* не падает, оставшиеся шаги выполняются.
*/
private async initBucket(): Promise<void> {
if (!this.fileStorage) {
this.logger.warn(
'File storage не настроен — пропускаем bucket init (PR #359 не вмержен)'
this.logger.info(
'File storage отключён конфигурацией — пропускаем bucket init'
);
return;
}