UNPKG

@chevre/domain

Version:

Chevre Domain Library for Node.js

428 lines (427 loc) 23 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.confirm = confirm; const createDebug = require("debug"); const moment = require("moment"); const errorHandler_1 = require("../../../errorHandler"); const factory = require("../../../factory"); const availableProductTypes_1 = require("../../../factory/availableProductTypes"); // import { Settings } from '../../../settings'; const factory_1 = require("../../order/placeOrder/factory"); const orderedItem_1 = require("../../order/placeOrder/factory/orderedItem"); const result_1 = require("./confirm/factory/result"); const potentialActions_1 = require("./confirm/potentialActions"); const publishCode_1 = require("./confirm/publishCode"); const validation_1 = require("./confirm/validation"); const publishConfirmationNumberIfNotExist_1 = require("./publishConfirmationNumberIfNotExist"); const publishOrderNumberIfNotExist_1 = require("./publishOrderNumberIfNotExist"); const debug = createDebug('chevre-domain:service:transaction'); function processTransactionNotInProgress(transaction) { return (repos) => __awaiter(this, void 0, void 0, function* () { if (transaction.status === factory.transactionStatusType.Confirmed) { // retrieve customer from orderInTransaction(2024-06-24~) // let resultCustomer: factory.order.ICustomer | undefined = createCustomer({ transaction }); const resultCustomer = yield repos.orderInTransaction.findCustomerByOrderNumber({ orderNumber: { $eq: String(transaction.object.orderNumber) } }); if (resultCustomer === undefined) { throw new factory.errors.Internal('resultCustomer undefined'); // impossible process } // すでに確定済の場合 return Object.assign(Object.assign({}, transaction.result), { customer: resultCustomer }); } else if (transaction.status === factory.transactionStatusType.Expired) { throw new factory.errors.Argument('transactionId', 'Transaction already expired'); } else if (transaction.status === factory.transactionStatusType.Canceled) { throw new factory.errors.Argument('transactionId', 'Transaction already canceled'); } }); } /** * 注文取引を確定する */ function confirm(params, options) { // tslint:disable-next-line:max-func-body-length return (repos // settings: Settings ) => __awaiter(this, void 0, void 0, function* () { var _a, _b; // 確認番号を事前発行 yield (0, publishConfirmationNumberIfNotExist_1.publishConfirmationNumberIfNotExist)({ id: params.id, status: { $in: [factory.transactionStatusType.Confirmed, factory.transactionStatusType.InProgress] }, object: { orderDate: params.result.order.orderDate } })(repos); const transaction = yield repos.transaction.projectFieldsById({ typeOf: factory.transactionType.PlaceOrder, id: params.id }, ['typeOf', 'project', 'status', 'agent', 'seller', 'object', 'result']); const confirmResult = yield processTransactionNotInProgress(transaction)(repos); if (confirmResult !== undefined) { return confirmResult; } if (typeof ((_a = params.agent) === null || _a === void 0 ? void 0 : _a.id) === 'string' && transaction.agent.id !== params.agent.id) { throw new factory.errors.Forbidden('Transaction not yours'); } // 取引に対する全ての承認アクションをマージ const completedAuthorizeActions = yield searchAuthorizeActions(params)(repos); let acceptPayActions = []; if (typeof ((_b = transaction.object.paymentMethods) === null || _b === void 0 ? void 0 : _b.paymentMethodId) === 'string') { acceptPayActions = yield searchAcceptPayActions(params)(repos); } // 注文番号を発行 const orderNumber = yield (0, publishOrderNumberIfNotExist_1.publishOrderNumberIfNotExist)({ project: { id: transaction.project.id }, id: transaction.id, object: { orderDate: params.result.order.orderDate } })(repos); // 必要あらば注文コード発行(2024-02-05~) let code; const publishCodeExpiresInSeconds = options.publishCodeExpiresInSeconds; if (typeof publishCodeExpiresInSeconds === 'number') { code = yield (0, publishCode_1.publishCode)({ project: { id: transaction.project.id }, object: { orderNumber, seller: { id: transaction.seller.id } }, validFrom: params.result.order.orderDate, expiresInSeconds: publishCodeExpiresInSeconds, author: { id: transaction.agent.id, typeOf: transaction.agent.typeOf } })(repos); } let acceptedOffers; const { // acceptedOffersFromAuthorizeAction, // discontinue(2024-06-17~) authorizePaymentActions, authorizeEventServiceOfferActions, authorizeMoneyTansferActions, authorizeProductOfferActions, serialNumbers } = dissolveAuthorizeActions(completedAuthorizeActions); // orderInTransactionから検索する(2024-03-04~) acceptedOffers = (yield repos.orderInTransaction.findAcceptedOffersByOrderNumber({ orderNumber: { $eq: orderNumber } })) .filter(({ serialNumber }) => typeof serialNumber === 'string' && serialNumbers.includes(serialNumber)); // authorizePaymentActionsからpayTransactionsを参照(2024-06-20~) let payTransactions = []; const payTransactionNumbers = authorizePaymentActions.map(({ instrument }) => instrument.transactionNumber); if (payTransactionNumbers.length > 0) { payTransactions = yield repos.assetTransaction.search({ typeOf: factory.assetTransactionType.Pay, transactionNumber: { $in: payTransactionNumbers } }, ['object']); } // retrieve customer from orderInTransaction(2024-06-24~) // let customer: factory.order.ICustomer = createCustomer({ transaction }); const customer = yield repos.orderInTransaction.findCustomerByOrderNumber({ orderNumber: { $eq: orderNumber } }); const seller = (0, factory_1.createSeller)({ transaction }); const { paymentMethods, result, eventId, reservationIds } = createResult(Object.assign(Object.assign(Object.assign({}, params), { orderNumber, transaction, acceptPayActions, authorizePaymentActions, authorizeEventServiceOfferActions, authorizeMoneyTansferActions, authorizeProductOfferActions, acceptedOffers, payTransactions, customer }), (typeof code === 'string') ? { code } : undefined), options); // デフォルトEメールメッセージを検索 let emailMessageOnOrderSent; if (repos.emailMessage !== undefined) { const searchEmailMessagesResult = yield repos.emailMessage.search({ limit: 1, page: 1, project: { id: { $eq: transaction.project.id } }, about: { identifier: { $eq: factory.creativeWork.message.email.AboutIdentifier.OnOrderSent } } }); emailMessageOnOrderSent = searchEmailMessagesResult.shift(); } // ポストアクションを作成 const setting = yield repos.setting.findOne({ project: { id: { $eq: '*' } } }, ['defaultSenderEmail']); if (typeof (setting === null || setting === void 0 ? void 0 : setting.defaultSenderEmail) !== 'string') { throw new factory.errors.NotFound('setting.defaultSenderEmail'); } const { emailMessages, potentialActions } = yield (0, potentialActions_1.createPotentialActions)(Object.assign(Object.assign({ order: result.order, // createEmailMessageでのorder.acceptedOffersへの依存性を排除したのでこちらでよいはず(2024-02-21~) customer, seller, paymentMethods }, (params.potentialActions !== undefined) ? { potentialActions: params.potentialActions } : undefined), (emailMessageOnOrderSent !== undefined) ? { emailMessage: emailMessageOnOrderSent } : undefined), // settings setting); // メッセージ保管(2024-04-28~) yield saveMessagesIfNeeded({ project: { id: transaction.project.id }, order: { orderNumber, typeOf: factory.order.OrderType.Order }, seller: { id: seller.id }, emailMessages })(repos); // ステータス変更 try { yield repos.transaction.confirm({ typeOf: transaction.typeOf, id: transaction.id, result: result, potentialActions: potentialActions }); } catch (error) { if (yield (0, errorHandler_1.isMongoError)(error)) { // 万が一同一注文番号で確定しようとすると、MongoDBでE11000 duplicate key errorが発生する // message: 'E11000 duplicate key error collection: prodttts.transactions index:result.order.orderNumber_1 dup key:...', if (error.code === errorHandler_1.MongoErrorCode.DuplicateKey) { throw new factory.errors.AlreadyInUse('transaction', ['result.order.orderNumber']); } } throw error; } const { order } = result; return Object.assign(Object.assign(Object.assign({ order, customer }, (typeof code === 'string') ? { code } : undefined), (typeof eventId === 'string') ? { eventId } : undefined), (Array.isArray(reservationIds)) ? { reservationIds } : undefined); }); } function saveMessagesIfNeeded(params) { return (repos) => __awaiter(this, void 0, void 0, function* () { const { project, order, emailMessages, seller } = params; if (emailMessages.length > 0) { const { orderNumber, typeOf } = order; // const saveMessageResult = await repos.message.upsertByIdentifier( // emailMessages.map(({ identifier, name, about, text, toRecipient, sender }) => { // return { // identifier, name, about, text, toRecipient, sender, // project: { id: project.id, typeOf: factory.organizationType.Project }, // provider: { id: seller.id, typeOf: factory.organizationType.Corporation }, // mainEntity: { orderNumber, typeOf } // }; // }) // ); for (const { identifier, name, about, text, toRecipient, sender } of emailMessages) { yield repos.message.upsertOneByIdentifier({ identifier, name, about, text, toRecipient, sender, project: { id: project.id, typeOf: factory.organizationType.Project }, provider: { id: seller.id, typeOf: factory.organizationType.Corporation }, mainEntity: { orderNumber, typeOf } }); } } }); } /** * 全承認アクションを分解する */ function dissolveAuthorizeActions(completedAuthorizeActions) { // const acceptedOffersFromAuthorizeAction: IOrderAcceptedOffer[] = []; // discontinue(2024-06-17~) const authorizePaymentActions = []; const authorizeEventServiceOfferActions = []; const authorizeMoneyTansferActions = []; const authorizeProductOfferActions = []; const serialNumbers = []; completedAuthorizeActions.forEach((a) => { var _a; let serialNumber; if (Array.isArray(a.object)) { if (a.object.length > 0 && a.object.every(({ typeOf }) => typeOf === factory.offerType.Offer) && a.object.every(({ itemOffered }) => availableProductTypes_1.availableProductTypes.indexOf(itemOffered.typeOf) >= 0)) { authorizeProductOfferActions.push(a); serialNumber = a.instrument.transactionNumber; } } else { if (a.object.typeOf === factory.action.authorize.offer.eventService.ObjectType.SeatReservation) { authorizeEventServiceOfferActions.push(a); const assetTransactionNumber = a.instrument.transactionNumber; if (typeof assetTransactionNumber !== 'string') { throw new factory.errors.NotFound('instrument.transactionNumber', `action.instrument.transactionNumber not found. [${a.id}]`); } serialNumber = assetTransactionNumber; } else if (a.object.typeOf === factory.offerType.Offer && ((_a = a.object.itemOffered) === null || _a === void 0 ? void 0 : _a.typeOf) === factory.actionType.MoneyTransfer) { authorizeMoneyTansferActions.push(a); serialNumber = a.instrument.transactionNumber; } else if (a.object.typeOf === factory.action.authorize.paymentMethod.any.ResultType.Payment) { authorizePaymentActions.push(a); } } if (typeof serialNumber === 'string') { serialNumbers.push(serialNumber); } }); debug('authorizeActions dissolved', serialNumbers, authorizeEventServiceOfferActions.length, 'authorizeEventServiceOfferActions', authorizePaymentActions.length, 'authorizePaymentActions'); return { authorizePaymentActions, authorizeEventServiceOfferActions, authorizeProductOfferActions, authorizeMoneyTansferActions, serialNumbers }; } function createReservationIdsResult(order, acceptedOffers) { var _a; let eventId = ''; const reservationIds = []; let eventIdsByOrder = (_a = order.orderedItem) === null || _a === void 0 ? void 0 : _a.filter((o) => o.orderedItem.typeOf === factory.product.ProductType.EventService).map((o) => { return o.orderedItem.serviceOutput.reservationFor.id; }); if (Array.isArray(eventIdsByOrder)) { eventIdsByOrder = [...new Set(eventIdsByOrder)]; // イベントIDが1つのみに対応 if (eventIdsByOrder.length === 1) { eventId = eventIdsByOrder[0]; if (Array.isArray(acceptedOffers)) { acceptedOffers.forEach((o) => { if (o.itemOffered.typeOf === factory.reservationType.EventReservation || o.itemOffered.typeOf === factory.reservationType.BusReservation) { reservationIds.push(o.itemOffered.id); } }); } } } return { eventId, reservationIds }; } // tslint:disable-next-line:max-func-body-length function createResult(params, options) { const transaction = params.transaction; const eventReservationAcceptedOffers = []; const productAcceptedOffers = []; const moneyTransferAcceptedOffers = []; params.acceptedOffers.forEach((acceptedOffer) => { const itemOfferedTypeOf = acceptedOffer.itemOffered.typeOf; switch (itemOfferedTypeOf) { case factory.reservationType.BusReservation: case factory.reservationType.EventReservation: eventReservationAcceptedOffers.push(acceptedOffer); break; case factory.permit.PermitType.Permit: productAcceptedOffers.push(acceptedOffer); break; case factory.actionType.MoneyTransfer: moneyTransferAcceptedOffers.push(acceptedOffer); break; default: throw new factory.errors.NotImplemented(`${itemOfferedTypeOf} not implemented`); } }); // 取引の確定条件が全て整っているかどうか確認 (0, validation_1.validateTransaction)(transaction, params.acceptPayActions, params.authorizePaymentActions, params.authorizeEventServiceOfferActions, params.authorizeMoneyTansferActions, params.authorizeProductOfferActions, eventReservationAcceptedOffers, params.payTransactions, params.customer); const { paymentMethods, price } = (0, factory_1.createPaymentMethods)({ authorizePaymentActions: params.authorizePaymentActions }); const orderedItem = (0, orderedItem_1.acceptedOffers2orderedItem)({ eventReservationAcceptedOffers, productAcceptedOffers, moneyTransferAcceptedOffers }); // 注文作成 const orderAsResult = (0, result_1.createOrderAsResult)({ orderNumber: params.orderNumber, orderDate: params.result.order.orderDate, orderStatus: factory.orderStatus.OrderPaymentDue, // 注文作成時のステータスとなる // orderedItem, // paymentMethods, price }); (0, validation_1.validateEventOffers)({ order: { price }, paymentMethods, authorizeEventServiceOfferActions: params.authorizeEventServiceOfferActions }); // 注文オファー検証 (0, validation_1.validateAcceptedOffers)({ result: params.result, acceptedOffers: params.acceptedOffers }); // 注文アイテム検証(2024-01-20~) (0, validation_1.validateOrderedItem)({ order: { orderedItem }, result: params.result }); (0, validation_1.validatePaymentMethods)({ order: { paymentMethods } }, options); // 確認番号を発行 const { confirmationNumber, identifier, url } = createConfirmationNumber({ order: orderAsResult, transaction: transaction, result: params.result }); orderAsResult.confirmationNumber = confirmationNumber; orderAsResult.identifier = identifier; orderAsResult.url = url; const authorizeActions = [ ...params.authorizePaymentActions.map(({ id }) => ({ id })), ...params.authorizeEventServiceOfferActions.map(({ id }) => ({ id })), ...params.authorizeProductOfferActions.map(({ id }) => ({ id })), ...params.authorizeMoneyTansferActions.map(({ id }) => ({ id })) ]; let eventId; let reservationIds; if (options.expectsReservationIds) { // ttts対応として予約IDリストとイベントIDを返却 const reservationIdsResult = createReservationIdsResult({ orderedItem }, params.acceptedOffers); eventId = reservationIdsResult.eventId; reservationIds = reservationIdsResult.reservationIds; } return Object.assign(Object.assign({ paymentMethods, result: Object.assign({ order: orderAsResult, authorizeActions, numAcceptedOffers: params.acceptedOffers.length }, (typeof params.code === 'string') ? { code: params.code } : undefined) }, (typeof eventId === 'string') ? { eventId } : undefined), (Array.isArray(reservationIds)) ? { reservationIds } : undefined); } function searchAcceptPayActions(params) { return (repos) => __awaiter(this, void 0, void 0, function* () { let acceptPayActions = yield repos.action.search({ project: { id: { $eq: params.project.id } }, typeOf: { $eq: factory.actionType.AcceptAction }, actionStatus: { $in: [factory.actionStatusType.CompletedActionStatus] }, purpose: { id: { $in: [params.id] }, typeOf: { $in: [factory.transactionType.PlaceOrder] } }, object: { typeOf: { $eq: factory.assetTransactionType.Pay } } }, ['object', 'endDate', 'result']); // 万が一このプロセス中に他処理が発生してもそれらを無視するように、endDateでフィルタリング acceptPayActions = acceptPayActions.filter((a) => { return (a.endDate instanceof Date) && moment(a.endDate) .isBefore(moment(params.result.order.orderDate)); }); return acceptPayActions; }); } function searchAuthorizeActions(params) { return (repos) => __awaiter(this, void 0, void 0, function* () { // 取引に対する全ての承認アクションをマージ let authorizeActions = yield repos.action.searchByPurpose({ typeOf: factory.actionType.AuthorizeAction, purpose: { typeOf: factory.transactionType.PlaceOrder, id: params.id }, // Completedに絞る(2023-05-16~) actionStatus: { $eq: factory.actionStatusType.CompletedActionStatus } }); // 万が一このプロセス中に他処理が発生してもそれらを無視するように、endDateでフィルタリング authorizeActions = authorizeActions.filter((a) => { return (a.endDate instanceof Date) && moment(a.endDate) .isBefore(moment(params.result.order.orderDate)); }); return authorizeActions; }); } function createConfirmationNumber(params) { const confirmationNumber = params.transaction.object.confirmationNumber; let url = ''; let identifier = []; // 取引に確認番号が保管されていなければ、確認番号を発行 if (typeof confirmationNumber !== 'string') { // 事前に発行済なはずなので、ありえないフロー throw new factory.errors.Internal('object.confirmationNumber undefined'); } // URLの指定があれば上書き // tslint:disable-next-line:no-single-line-block-comment /* istanbul ignore if */ if (typeof params.result.order.url === 'string') { url = params.result.order.url; } else /* istanbul ignore next */ if (typeof params.result.order.url === 'function') { // tslint:disable-next-line:no-single-line-block-comment /* istanbul ignore next */ url = params.result.order.url(params.order); } // 識別子の指定があれば上書き identifier = [ ...(Array.isArray(params.result.order.identifier)) ? params.result.order.identifier : [], // 取引に指定があれば追加 ...(Array.isArray(params.transaction.object.identifier)) ? params.transaction.object.identifier : [] ]; return { confirmationNumber, url, identifier }; }