diff --git a/api/v1/submissions/PKPSubmissionHandler.inc.php b/api/v1/submissions/PKPSubmissionHandler.inc.php index a72ca2d0ac9..7e029ffb3d5 100644 --- a/api/v1/submissions/PKPSubmissionHandler.inc.php +++ b/api/v1/submissions/PKPSubmissionHandler.inc.php @@ -927,7 +927,7 @@ public function addDecision($slimRequest, $response, $args) $params['editorId'] = $request->getUser()->getId(); $params['stageId'] = $type->getStageId(); - $errors = Repo::decision()->validate($params, $type, $submission); + $errors = Repo::decision()->validate($params, $type, $submission, $request->getContext()); if (!empty($errors)) { return $response->withStatus(400)->withJson($errors); diff --git a/classes/components/forms/decision/RecommendDiscussionForm.inc.php b/classes/components/forms/decision/RecommendDiscussionForm.inc.php deleted file mode 100644 index 874d2865bb6..00000000000 --- a/classes/components/forms/decision/RecommendDiscussionForm.inc.php +++ /dev/null @@ -1,63 +0,0 @@ -addField(new FieldRichTextarea('recommendation', [ - 'label' => __('editor.review.newReviewRound'), - 'type' => 'radio', - 'options' => [ - [ - 'value' => RequestRevisions::class, - 'label' => __('editor.review.NotifyAuthorRevisions'), - ], - [ - 'value' => Resubmit::class, - 'label' => __('editor.review.NotifyAuthorResubmit'), - ], - ], - 'value' => RequestRevisions::class, - 'groupId' => 'default', - ])) - ->addGroup([ - 'id' => 'default', - 'pageId' => 'default', - ]) - ->addPage([ - 'id' => 'default', - 'submitButton' => ['label' => __('help.next')] - ]); - } -} diff --git a/classes/components/forms/decision/RequestPaymentDecisionForm.inc.php b/classes/components/forms/decision/RequestPaymentDecisionForm.inc.php new file mode 100644 index 00000000000..498a8c69005 --- /dev/null +++ b/classes/components/forms/decision/RequestPaymentDecisionForm.inc.php @@ -0,0 +1,56 @@ +addField(new FieldOptions('requestPayment', [ + 'label' => __('common.payment'), + 'type' => 'radio', + 'options' => [ + [ + 'value' => true, + 'label' => __( + 'payment.requestPublicationFee', + ['feeAmount' => $context->getData('publicationFee') . ' ' . $context->getData('currency')] + ), + ], + [ + 'value' => false, + 'label' => __('payment.waive'), + ], + ], + 'value' => true, + 'groupId' => 'default', + ])); + } +} diff --git a/classes/decision/Repository.inc.php b/classes/decision/Repository.inc.php index 92188c863c4..a34f2d2a531 100644 --- a/classes/decision/Repository.inc.php +++ b/classes/decision/Repository.inc.php @@ -22,6 +22,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\App; use Illuminate\Support\LazyCollection; +use PKP\context\Context; use PKP\core\Core; use PKP\db\DAORegistry; use PKP\log\PKPSubmissionEventLogEntry; @@ -113,7 +114,7 @@ public function getSchemaMap(): maps\Schema * * @return array A key/value array with validation errors. Empty if no errors */ - public function validate(array $props, Type $type, Submission $submission): array + public function validate(array $props, Type $type, Submission $submission, Context $context): array { AppLocale::requireComponents( LOCALE_COMPONENT_PKP_EDITOR, @@ -145,7 +146,7 @@ public function validate(array $props, Type $type, Submission $submission): arra [] ); - $validator->after(function ($validator) use ($props, $type, $submission) { + $validator->after(function ($validator) use ($props, $type, $submission, $context) { // The decision stage id must match the decision type's stage id // and the submission's current workflow stage @@ -185,7 +186,7 @@ public function validate(array $props, Type $type, Submission $submission): arra } // Allow the decision type to add validation checks - $type->validate($props, $submission, $validator, isset($reviewRound) ? $reviewRound->getId() : null); + $type->validate($props, $submission, $context, $validator, isset($reviewRound) ? $reviewRound->getId() : null); }); $errors = []; diff --git a/classes/decision/Type.inc.php b/classes/decision/Type.inc.php index cda66abde32..91de1976889 100644 --- a/classes/decision/Type.inc.php +++ b/classes/decision/Type.inc.php @@ -17,14 +17,17 @@ use APP\facades\Repo; use APP\submission\Submission; use Exception; +use Illuminate\Support\Facades\App; use Illuminate\Validation\Validator; use PKP\context\Context; use PKP\db\DAORegistry; use PKP\security\Role; +use PKP\services\PKPSchemaService; use PKP\submission\reviewAssignment\ReviewAssignment; use PKP\submission\reviewRound\ReviewRound; use PKP\submission\reviewRound\ReviewRoundDAO; use PKP\user\User; +use PKP\validation\ValidatorFactory; abstract class Type { @@ -112,17 +115,13 @@ public function isInReview(): bool } /** - * A callback method that is fired when a decision - * of this type is being validated - * - * Use this method to validate any custom data that may be - * required for this decision. - * - * Add a validation error: + * Validate this decision * - * $validator->errors()->add('requestPayment', __('validator.required')); + * The default decision properties will already be validated. Use + * this method to validate data for this decision's actions, or + * to apply any additional restrictions for this decision. */ - public function validate(array $props, Submission $submission, Validator $validator) + public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null) { // No validation checks are performed by default } @@ -225,6 +224,79 @@ protected function getCompletedReviewerIds(Submission $submission, int $reviewRo return $userIds; } + /** + * Validate the properties of an email action + * + * @return array Empty if no errors + */ + protected function validateEmailAction(array $emailAction): array + { + $schema = (object) [ + 'bcc' => (object) [ + 'type' => 'array', + 'items' => (object) [ + 'type' => 'string', + 'validation' => [ + 'email_or_localhost', + ], + ], + ], + 'body' => (object) [ + 'type' => 'string', + 'validation' => [ + 'required', + ], + ], + 'cc' => (object) [ + 'type' => 'array', + 'items' => (object) [ + 'type' => 'string', + 'validation' => [ + 'email_or_localhost', + ], + ], + ], + 'id' => (object) [ + 'type' => 'string', + 'validation' => [ + 'alpha', + 'required', + ], + ], + 'subject' => (object) [ + 'type' => 'string', + 'validation' => [ + 'required', + ], + ], + 'to' => (object) [ + 'type' => 'array', + 'items' => (object) [ + 'type' => 'integer', + ], + ], + ]; + + $schemaService = App::make(PKPSchemaService::class); + $rules = []; + foreach ($schema as $propName => $propSchema) { + $rules = $schemaService->addPropValidationRules($rules, $propName, $propSchema); + } + + $validator = ValidatorFactory::make( + $emailAction, + $rules, + ); + + $errors = []; + + if ($validator->fails()) { + $errors = $schemaService->formatValidationErrors($validator->errors()); + } + + return $errors; + } + /** * Set an error message for invalid recipients * diff --git a/classes/decision/types/Accept.inc.php b/classes/decision/types/Accept.inc.php index 751841efba8..0adb9a02979 100644 --- a/classes/decision/types/Accept.inc.php +++ b/classes/decision/types/Accept.inc.php @@ -17,12 +17,15 @@ use APP\core\Services; use APP\facades\Repo; use APP\log\SubmissionEventLogEntry; +use APP\notification\Notification; +use APP\notification\NotificationManager; +use APP\payment\ojs\OJSPaymentManager; use APP\submission\Submission; use APP\workflow\EditorDecisionActionsManager; use Exception; use Illuminate\Support\Facades\Mail; use Illuminate\Validation\Validator; -use PKP\components\forms\decision\SelectRevisionDecisionForm; +use PKP\components\forms\decision\RequestPaymentDecisionForm; use PKP\context\Context; use PKP\decision\Decision; use PKP\decision\steps\Email; @@ -86,7 +89,7 @@ public function getDescription(): string return __('editor.submission.decision.accept.description'); } - public function validate(array $props, Submission $submission, Validator $validator, ?int $reviewRoundId = null) + public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null) { // If there is no review round id, a validation error will already have been set if (!$reviewRoundId) { @@ -95,7 +98,29 @@ public function validate(array $props, Submission $submission, Validator $valida foreach ($props['actions'] as $index => $action) { switch ($action['id']) { + case self::ACTION_PAYMENT: + $paymentManager = Application::getPaymentManager($context); + if (!$paymentManager->publicationEnabled()) { + $validator->errors()->add('actions.' . $index, __('payment.requestPublicationFee.notEnabled')); + } elseif (!isset($action['requestPayment'])) { + $validator->errors()->add('actions.' . $index, __('validator.required')); + } + break; + case self::ACTION_NOTIFY_AUTHORS: + case self::ACTION_NOTIFY_REVIEWERS: + $errors = $this->validateEmailAction($action); + if (count($errors)) { + foreach ($errors as $key => $error) { + $validator->errors()->add('actions.' . $index . '.' . $key, $error); + } + break; + } + // no break case self::ACTION_NOTIFY_REVIEWERS: + if (empty($action['to'])) { + $validator->errors()->add('actions.' . $index . '.to', __('validator.required')); + break; + } $reviewerIds = $this->getCompletedReviewerIds($submission, $reviewRoundId); $invalidRecipients = array_diff($action['to'], $reviewerIds); if (count($invalidRecipients)) { @@ -116,8 +141,7 @@ public function callback(Decision $decision, Submission $submission, User $edito foreach ($actions as $action) { switch ($action['id']) { case self::ACTION_PAYMENT: - // TODO: implement queued payments - error_log(print_r($action, true)); + $this->requestPayment($submission, $editor, $context); break; case self::ACTION_NOTIFY_AUTHORS: $this->sendAuthorEmail( @@ -153,13 +177,16 @@ public function getWorkflow(Submission $submission, Context $context, ?int $revi Services::get('emailTemplate')->getByKey($context->getId(), 'EDITOR_DECISION_SEND_TO_PRODUCTION'), ]; - - $workflow->addStep(new Form( - self::ACTION_PAYMENT, - __('editor.article.payment.requestPayment'), - '', - new SelectRevisionDecisionForm() - )); + // Request payment if configured + $paymentManager = Application::getPaymentManager($context); + if ($paymentManager->publicationEnabled()) { + $workflow->addStep(new Form( + self::ACTION_PAYMENT, + __('editor.article.payment.requestPayment'), + '', + new RequestPaymentDecisionForm($context) + )); + } $workflow->addStep(new Email( self::ACTION_NOTIFY_AUTHORS, @@ -264,4 +291,36 @@ protected function sendReviewerEmail(stdClass $email, Decision $decision, Submis ] ); } + + /** + * Request payment from authors + */ + protected function requestPayment(Submission $submission, User $editor, Context $context) + { + $paymentManager = Application::getPaymentManager($context); + $queuedPayment = $paymentManager->createQueuedPayment( + Application::get()->getRequest(), + OJSPaymentManager::PAYMENT_TYPE_PUBLICATION, + $editor->getId(), + $submission->getId(), + $context->getData('publicationFee'), + $context->getData('currency') + ); + $paymentManager->queuePayment($queuedPayment); + + // Notify authors that this needs payment. + $notificationMgr = new NotificationManager(); + $authorIds = $this->getAssignedAuthorIds($submission); + foreach ($authorIds as $authorId) { + $notificationMgr->createNotification( + Application::get()->getRequest(), + $authorId, + Notification::NOTIFICATION_TYPE_PAYMENT_REQUIRED, + $context->getId(), + Application::ASSOC_TYPE_QUEUED_PAYMENT, + $queuedPayment->getId(), + Notification::NOTIFICATION_LEVEL_TASK + ); + } + } } diff --git a/classes/decision/types/RecommendAccept.inc.php b/classes/decision/types/RecommendAccept.inc.php index 82aeeb25be3..f31a5184447 100644 --- a/classes/decision/types/RecommendAccept.inc.php +++ b/classes/decision/types/RecommendAccept.inc.php @@ -16,6 +16,7 @@ use APP\core\Services; use APP\submission\Submission; use APP\workflow\EditorDecisionActionsManager; +use Illuminate\Validation\Validator; use PKP\context\Context; use PKP\db\DAORegistry; use PKP\decision\Decision; @@ -74,6 +75,24 @@ public function getMailable(): string return AcceptedExample::class; } + public function validate(array $props, Submission $submission, Context $context, Validator $validator, ?int $reviewRoundId = null) + { + foreach ($props['actions'] as $index => $action) { + switch ($action['id']) { + case self::ACTION_DISCUSSION: + $errors = $this->validateEmailAction($action); + if (count($errors)) { + foreach ($errors as $key => $error) { + $validator->errors()->add('actions.' . $index . '.' . $key, $error); + } + } + break; + } + } + } + + + public function callback(Decision $decision, Submission $submission, User $editor, Context $context, array $actions) { parent::callback($decision, $submission, $editor, $context, $actions); diff --git a/classes/payment/PaymentManager.inc.php b/classes/payment/PaymentManager.inc.php index 2281bb9df1c..aa05147a4db 100644 --- a/classes/payment/PaymentManager.inc.php +++ b/classes/payment/PaymentManager.inc.php @@ -18,6 +18,8 @@ namespace PKP\payment; +use PKP\db\DAORegistry; + abstract class PaymentManager { public const PAYMENT_TYPE_PUBLICATION = 7; // FIXME: This is OJS-only but referred to in pkp-lib. Move back to OJS. diff --git a/classes/payment/QueuedPaymentDAO.inc.php b/classes/payment/QueuedPaymentDAO.inc.php index 7c9603783dc..52996fd2a4e 100644 --- a/classes/payment/QueuedPaymentDAO.inc.php +++ b/classes/payment/QueuedPaymentDAO.inc.php @@ -18,6 +18,9 @@ namespace PKP\payment; +use PKP\core\Core; +use PKP\db\DAORegistry; + class QueuedPaymentDAO extends \PKP\db\DAO { /** diff --git a/schemas/decision.json b/schemas/decision.json index b4cf4bb6e00..7a16aefc378 100644 --- a/schemas/decision.json +++ b/schemas/decision.json @@ -18,55 +18,8 @@ }, "actions": { "type": "array", - "description": "A list of actions to be taken with this decision, such as sending an email. Each decision supports different actions. The properties below are used with common actions but do not cover all decisions.", - "writeOnly": true, - "items": { - "type": "object", - "properties": { - "bcc": { - "type": "array", - "description": "Email addresses to be bcc'd. Only used with an email action.", - "items": { - "type": "string", - "validation": [ - "email_or_localhost" - ] - } - }, - "body": { - "type": "string", - "description": "The email's message body. Only used with an email action." - }, - "cc": { - "type": "array", - "description": "Email addresses to be cc'd. Only used with an email action.", - "items": { - "type": "string", - "validation": [ - "email_or_localhost" - ] - } - }, - "id": { - "type": "string", - "description": "The id must match one of the actions supported by the decision type. If the decision type does not recognize the id, the action will not be processed.", - "validation": [ - "alpha" - ] - }, - "subject": { - "type": "string", - "description": "The email's subject. Only used with an email action." - }, - "to": { - "type": "array", - "description": "A list of user IDs the email should be sent to. Only used with an email action.", - "items": { - "type": "integer" - } - } - } - } + "description": "A list of actions to be taken with this decision, such as sending an email. Each decision supports different actions with different properties. See the examples for support decision actions.", + "writeOnly": true }, "dateDecided": { "type": "string", diff --git a/styles/pages/workflow.less b/styles/pages/workflow.less index 32a4d306af9..8a745b56f2b 100644 --- a/styles/pages/workflow.less +++ b/styles/pages/workflow.less @@ -110,6 +110,7 @@ .pkp_button { width: 100%; + text-align: center; } } diff --git a/templates/decision/record.tpl b/templates/decision/record.tpl index 429fc9e6dc4..ba94b8619df 100644 --- a/templates/decision/record.tpl +++ b/templates/decision/record.tpl @@ -119,7 +119,7 @@
- {{ localize(item.name) }} + {{ item.id }} — {{ localize(item.name) }}