diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index da3b6fcab..dc51621c5 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -559,60 +559,61 @@ async downloadBulkIssuanceCSVTemplate( * @param res * @returns Issuer creates a credential offer and sends it to the holder */ - @Post('/orgs/:orgId/credentials/offer') - @ApiBearerAuth() - @ApiOperation({ - summary: `Issuer create a credential offer`, - description: `Issuer creates a credential offer and sends it to the holder` - }) - @ApiQuery({ - name:'credentialType', - enum: IssueCredentialType - }) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER) - @ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto }) - async sendCredential( - @User() user: IUserRequest, - @Param('orgId', new ParseUUIDPipe({exceptionFactory: (): Error => { throw new BadRequestException(`Invalid format for orgId`); }})) orgId: string, - @Query('credentialType') credentialType: IssueCredentialType = IssueCredentialType.INDY, - @Body() issueCredentialDto: IssueCredentialDto, - @Res() res: Response - ): Promise { - issueCredentialDto.orgId = orgId; - issueCredentialDto.credentialType = credentialType; - - const credOffer = issueCredentialDto?.credentialData || []; - - if (IssueCredentialType.INDY !== credentialType && IssueCredentialType.JSONLD !== credentialType) { - throw new NotFoundException(ResponseMessages.issuance.error.invalidCredentialType); - } - - if (credentialType === IssueCredentialType.INDY && !issueCredentialDto.credentialDefinitionId) { - throw new BadRequestException(ResponseMessages.credentialDefinition.error.isRequired); - } - - if (issueCredentialDto.credentialType !== IssueCredentialType.INDY && !credOffer.every(offer => (!offer?.attributes || 0 === Object.keys(offer?.attributes).length))) { - throw new BadRequestException(ResponseMessages.issuance.error.attributesAreRequired); - } - - if (issueCredentialDto.credentialType === IssueCredentialType.JSONLD && credOffer.every(offer => (!offer?.credential || 0 === Object.keys(offer?.credential).length))) { - throw new BadRequestException(ResponseMessages.issuance.error.credentialNotPresent); - } - - if (issueCredentialDto.credentialType === IssueCredentialType.JSONLD && credOffer.every(offer => (!offer?.options || 0 === Object.keys(offer?.options).length))) { - throw new BadRequestException(ResponseMessages.issuance.error.optionsNotPresent); - } - const getCredentialDetails = await this.issueCredentialService.sendCredentialCreateOffer(issueCredentialDto, user); - - const finalResponse: IResponse = { - statusCode: HttpStatus.CREATED, - message: ResponseMessages.issuance.success.create, - data: getCredentialDetails - }; - return res.status(HttpStatus.CREATED).json(finalResponse); - } - + @Post('/orgs/:orgId/credentials/offer') + @ApiBearerAuth() + @ApiOperation({ + summary: `Issuer create a credential offer`, + description: `Issuer creates a credential offer and sends it to the holder` + }) + @ApiQuery({ + name:'credentialType', + enum: IssueCredentialType + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Created', type: ApiResponseDto }) + async sendCredential( + @User() user: IUserRequest, + @Param('orgId', new ParseUUIDPipe({exceptionFactory: (): Error => { throw new BadRequestException(`Invalid format for orgId`); }})) orgId: string, + @Body() issueCredentialDto: IssueCredentialDto, + @Res() res: Response, + @Query('credentialType') credentialType: IssueCredentialType = IssueCredentialType.INDY + ): Promise { + issueCredentialDto.orgId = orgId; + issueCredentialDto.credentialType = credentialType; + + const credOffer = issueCredentialDto?.credentialData || []; + + if (IssueCredentialType.INDY !== credentialType && IssueCredentialType.JSONLD !== credentialType) { + throw new NotFoundException(ResponseMessages.issuance.error.invalidCredentialType); + } + + if (credentialType === IssueCredentialType.INDY && !issueCredentialDto.credentialDefinitionId) { + throw new BadRequestException(ResponseMessages.credentialDefinition.error.isRequired); + } + + if (issueCredentialDto.credentialType !== IssueCredentialType.INDY && !credOffer.every(offer => (!offer?.attributes || 0 === Object.keys(offer?.attributes).length))) { + throw new BadRequestException(ResponseMessages.issuance.error.attributesAreRequired); + } + + if (issueCredentialDto.credentialType === IssueCredentialType.JSONLD && credOffer.every(offer => (!offer?.credential || 0 === Object.keys(offer?.credential).length))) { + throw new BadRequestException(ResponseMessages.issuance.error.credentialNotPresent); + } + + if (issueCredentialDto.credentialType === IssueCredentialType.JSONLD && credOffer.every(offer => (!offer?.options || 0 === Object.keys(offer?.options).length))) { + throw new BadRequestException(ResponseMessages.issuance.error.optionsNotPresent); + } + const getCredentialDetails = await this.issueCredentialService.sendCredentialCreateOffer(issueCredentialDto, user); + const { statusCode, message, data} = getCredentialDetails; + + const finalResponse: IResponse = { + statusCode, + message, + data + }; + + return res.status(statusCode).json(finalResponse); + } /** * * @param user diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index 28316fc0e..f0f8658a5 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -5,7 +5,7 @@ import { BaseService } from 'libs/service/base.service'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { ClientDetails, FileParameter, IssuanceDto, OOBCredentialDtoWithEmail, OOBIssueCredentialDto, PreviewFileDetails, TemplateDetails } from './dtos/issuance.dto'; import { FileExportResponse, IIssuedCredentialSearchParams, IReqPayload, ITemplateFormat, IssueCredentialType, UploadedFileDetails } from './interfaces'; -import { IIssuedCredential } from '@credebl/common/interfaces/issuance.interface'; +import { ICredentialOfferResponse, IIssuedCredential } from '@credebl/common/interfaces/issuance.interface'; import { IssueCredentialDto } from './dtos/multi-connection.dto'; @Injectable() @@ -18,7 +18,7 @@ export class IssuanceService extends BaseService { super('IssuanceService'); } - sendCredentialCreateOffer(issueCredentialDto: IssueCredentialDto, user: IUserRequest): Promise { + sendCredentialCreateOffer(issueCredentialDto: IssueCredentialDto, user: IUserRequest): Promise { const payload = { comment: issueCredentialDto.comment, credentialDefinitionId: issueCredentialDto.credentialDefinitionId, credentialData: issueCredentialDto.credentialData, orgId: issueCredentialDto.orgId, protocolVersion: issueCredentialDto.protocolVersion, autoAcceptCredential: issueCredentialDto.autoAcceptCredential, credentialType: issueCredentialDto.credentialType, user }; diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts index 1a3c33594..e6f1b992c 100644 --- a/apps/issuance/src/issuance.controller.ts +++ b/apps/issuance/src/issuance.controller.ts @@ -1,8 +1,8 @@ import { Controller } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; -import { IClientDetails, ICreateOfferResponse, IIssuance, IIssueCredentials, IIssueCredentialsDefinitions, ImportFileDetails, IssueCredentialWebhookPayload, OutOfBandCredentialOffer, PreviewRequest, TemplateDetailsInterface } from '../interfaces/issuance.interfaces'; +import { IClientDetails, IIssuance, IIssueCredentials, IIssueCredentialsDefinitions, ImportFileDetails, IssueCredentialWebhookPayload, OutOfBandCredentialOffer, PreviewRequest, TemplateDetailsInterface } from '../interfaces/issuance.interfaces'; import { IssuanceService } from './issuance.service'; -import { IIssuedCredential } from '@credebl/common/interfaces/issuance.interface'; +import { ICredentialOfferResponse, IIssuedCredential } from '@credebl/common/interfaces/issuance.interface'; import { OOBIssueCredentialDto } from 'apps/api-gateway/src/issuance/dtos/issuance.dto'; @Controller() @@ -16,7 +16,7 @@ export class IssuanceController { } @MessagePattern({ cmd: 'send-credential-create-offer' }) - async sendCredentialCreateOffer(payload: IIssuance): Promise[]> { + async sendCredentialCreateOffer(payload: IIssuance): Promise { return this.issuanceService.sendCredentialCreateOffer(payload); } diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 71b85ca2f..242ce2661 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -1,7 +1,7 @@ /* eslint-disable no-useless-catch */ /* eslint-disable camelcase */ import { CommonService } from '@credebl/common'; -import { BadRequestException, ConflictException, HttpException, Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; +import { BadRequestException, ConflictException, HttpException, HttpStatus, Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; import { IssuanceRepository } from './issuance.repository'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { CommonConstants } from '@credebl/common/common.constant'; @@ -9,7 +9,7 @@ import { ResponseMessages } from '@credebl/common/response-messages'; import { ClientProxy, RpcException } from '@nestjs/microservices'; import { map } from 'rxjs'; import { CredentialOffer, FileUpload, FileUploadData, IAttributes, IClientDetails, ICreateOfferResponse, ICredentialPayload, IIssuance, IIssueData, IPattern, IQueuePayload, ISendOfferNatsPayload, ImportFileDetails, IssueCredentialWebhookPayload, OutOfBandCredentialOfferPayload, PreviewRequest, SchemaDetails, SendEmailCredentialOffer, TemplateDetailsInterface } from '../interfaces/issuance.interfaces'; -import { OrgAgentType, SchemaType, TemplateIdentifier } from '@credebl/enum/enum'; +import { OrgAgentType, PromiseResult, SchemaType, TemplateIdentifier } from '@credebl/enum/enum'; import * as QRCode from 'qrcode'; import { OutOfBandIssuance } from '../templates/out-of-band-issuance.template'; import { EmailDto } from '@credebl/common/dtos/email.dto'; @@ -27,7 +27,7 @@ import { FileUploadStatus, FileUploadType } from 'apps/api-gateway/src/enum'; import { AwsService } from '@credebl/aws'; import { io } from 'socket.io-client'; import { IIssuedCredentialSearchParams, IssueCredentialType } from 'apps/api-gateway/src/issuance/interfaces'; -import { IIssuedCredential, IJsonldCredential } from '@credebl/common/interfaces/issuance.interface'; +import { ICredentialOfferResponse, IIssuedCredential, IJsonldCredential } from '@credebl/common/interfaces/issuance.interface'; import { OOBIssueCredentialDto } from 'apps/api-gateway/src/issuance/dtos/issuance.dto'; import { agent_invitations, organisation } from '@prisma/client'; import { createOobJsonldIssuancePayload, validateEmail } from '@credebl/common/cast.helper'; @@ -63,7 +63,7 @@ export class IssuanceService { } } - async sendCredentialCreateOffer(payload: IIssuance): Promise[]> { + async sendCredentialCreateOffer(payload: IIssuance): Promise { try { const { orgId, credentialDefinitionId, comment, credentialData } = payload || {}; @@ -145,7 +145,49 @@ export class IssuanceService { }); const results = await Promise.allSettled(issuancePromises); - return results; + + const processedResults = results.map((result) => { + if (PromiseResult.REJECTED === result.status) { + return { + statusCode: result?.reason?.status?.statusCode, + message: result?.reason?.status?.message?.error?.message, + error: result?.reason?.status?.error + }; + } else if (PromiseResult.FULFILLED === result.status) { + return { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.issuance.success.create, + data: result.value + }; + } + return null; + }); + + const allSuccessful = processedResults.every((result) => result?.statusCode === HttpStatus.CREATED); + const allFailed = processedResults.every((result) => result?.statusCode !== HttpStatus.CREATED); + + let finalStatusCode: HttpStatus; + let finalMessage: string; + + if (allSuccessful) { + finalStatusCode = HttpStatus.CREATED; + finalMessage = ResponseMessages.issuance.success.create; + } else if (allFailed) { + finalStatusCode = HttpStatus.BAD_REQUEST; + finalMessage = ResponseMessages.issuance.error.unableToCreateOffer; + } else { + finalStatusCode = HttpStatus.PARTIAL_CONTENT; + finalMessage = ResponseMessages.issuance.success.partiallyOfferCreated; + } + + const finalResult = { + statusCode: finalStatusCode, + message: finalMessage, + data: processedResults + }; + + return finalResult; + } catch (error) { this.logger.error(`[sendCredentialCreateOffer] - error in create credentials : ${JSON.stringify(error)}`); const errorStack = error?.status?.message?.error?.reason || error?.status?.message?.error; @@ -599,10 +641,10 @@ async outOfBandCredentialOffer(outOfBandCredential: OutOfBandCredentialOfferPayl ); if (0 < error?.length) { const errorStack = error?.map((item) => { - const { message, statusCode, error } = item?.error || item?.response || {}; + const { statusCode, message, error } = item?.error || item?.response || {}; return { - message, statusCode, + message, error }; }); diff --git a/apps/ledger/src/schema/schema.service.ts b/apps/ledger/src/schema/schema.service.ts index 3cc32c1a4..7681e1874 100644 --- a/apps/ledger/src/schema/schema.service.ts +++ b/apps/ledger/src/schema/schema.service.ts @@ -515,7 +515,7 @@ export class SchemaService extends BaseService { const { type, title } = schemaAttributeJson[key]; const schemaDataType = type; const displayName = title; - const isRequired = false; + const isRequired = true; extractedData.push({ 'attributeName': title, schemaDataType, displayName, isRequired }); } } diff --git a/libs/common/src/interfaces/issuance.interface.ts b/libs/common/src/interfaces/issuance.interface.ts index 1b7691b1c..475bf4e56 100644 --- a/libs/common/src/interfaces/issuance.interface.ts +++ b/libs/common/src/interfaces/issuance.interface.ts @@ -26,4 +26,48 @@ export interface IIssuedCredential { credentialData: CredentialData orgDid: string; orgId: string; - } \ No newline at end of file + } + + export interface ICredentialOfferResponse { + statusCode: number; + message: string; + data: ICredentialOfferData[]; + } + + interface ICredentialOfferData { + statusCode: number; + message: string; + error?: string; + data?: ICredentialOfferDetails; + } + + interface ICredentialAttribute { + 'mime-type': string; + name: string; + value: string; + } + + interface ICredentialOfferDetails { + _tags?: { + connectionId: string; + state: string; + threadId: string; + }; + metadata?: { + '_anoncreds/credential'?: { + schemaId: string; + credentialDefinitionId: string; + }; + }; + credentials?: unknown[]; + id: string; + createdAt: string; + state: string; + connectionId: string; + threadId: string; + protocolVersion: string; + credentialAttributes?: ICredentialAttribute[]; + autoAcceptCredential?: string; + contextCorrelationId?: string; + } + \ No newline at end of file diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 2c56588aa..2d87e52af 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -275,6 +275,7 @@ export const ResponseMessages = { issuance: { success: { create: 'Credentials offer created successfully', + partiallyOfferCreated: 'Credential offer created partially', createOOB: 'Out-of-band credentials offer created successfully', fetch: 'Issued Credential details fetched successfully', importCSV: 'File imported successfully', diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 3ed5d31d7..0dadfb00c 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -138,4 +138,9 @@ export enum IssueCredentialType { export enum TemplateIdentifier { EMAIL_COLUMN = 'email_identifier' +} + +export enum PromiseResult { + REJECTED = 'rejected', + FULFILLED = 'fulfilled' } \ No newline at end of file