latteEngine = $latteEngine; $this->formFactory = $formFactory; $this->customsDeclarationRepository = $customsDeclarationRepository; $this->customsDeclarationEntityFactory = $customsDeclarationEntityFactory; $this->request = $request; $this->messageManager = $messageManager; $this->detailCommonLogic = $detailCommonLogic; $this->wpAdapter = $wpAdapter; } /** * Registers related hooks. * * @return void */ public function register(): void { add_action( 'add_meta_boxes', [ $this, 'addMetaBoxes' ] ); add_action( 'admin_head', [ $this, 'renderTemplate' ] ); } /** * Adds meta boxes. * * @return void */ public function addMetaBoxes(): void { $order = $this->detailCommonLogic->getOrder(); if ( $order === null || $order->getCarrier()->requiresCustomsDeclarations() === false ) { return; } add_meta_box( 'packetery_customs_declaration_metabox', __( 'Customs declaration', 'packeta' ), [ $this, 'render' ], ModuleHelper::isHposEnabled() ? wc_get_page_screen_id( 'shop-order' ) : 'shop_order', 'advanced', 'high' ); } /** * Renders template. * * @return void */ public function renderTemplate(): void { $formTemplate = $this->formFactory->create(); $prefixContainer = $formTemplate->addContainer( self::FORM_CONTAINER_NAME ); $items = $prefixContainer->addContainer( 'items' ); $this->addCustomsDeclarationItem( $formTemplate->addCheckbox( self::FORM_ACTIVATOR_NAME ), $items, '0' ); $this->latteEngine->render( PACKETERY_PLUGIN_DIR . '/template/order/customs-declaration-form-template.latte', [ 'formTemplate' => $formTemplate, 'translations' => [ 'delete' => __( 'Delete', 'packeta' ), ], ] ); } /** * Saves submitted form fields data. * * @param Order $order Order ID. * @return void */ public function saveFields( Order $order ): void { if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || $this->request->getPost( self::FORM_CONTAINER_NAME ) === null ) { return; } $form = $this->createForm( $this->request->getPost(), $this->customsDeclarationRepository->getByOrderNumber( $order->getNumber() ) ); if ( $form->isSubmitted() ) { $form->fireEvents(); } } /** * Renders meta box. * * @return void */ public function render(): void { $order = $this->detailCommonLogic->getOrder(); if ( $order === null ) { return; } $customsDeclaration = $this->customsDeclarationRepository->getByOrderNumber( $order->getNumber() ); $formData = []; if ( $customsDeclaration !== null ) { $formData = [ self::FORM_CONTAINER_NAME => $this->customsDeclarationRepository->declarationToDbArray( $customsDeclaration, [ 'invoice_file', 'ead_file', 'order_id' ] ), ]; $formData[ self::FORM_CONTAINER_NAME ]['items'] = []; $customsDeclarationItems = $this->customsDeclarationRepository->getItemsByCustomsDeclarationId( $customsDeclaration->getId() ); foreach ( $customsDeclarationItems as $customsDeclarationItem ) { $formData[ self::FORM_CONTAINER_NAME ]['items'][ $customsDeclarationItem->getId() ] = $this->customsDeclarationRepository->declarationItemToDbArray( $customsDeclarationItem ); } } $form = $this->createForm( $formData, $customsDeclaration ); $form->setDefaults( $formData ); $hasInvoiceFile = $customsDeclaration !== null && $customsDeclaration->hasInvoiceFileContent(); $hasEadFile = $customsDeclaration !== null && $customsDeclaration->hasEadFileContent(); $runWizardUrl = $this->wpAdapter->adminUrl( "admin.php?page=wc-orders&action=edit&id={$order->getNumber()}&wizard-enabled=true&wizard-order-detail-custom-declaration-enabled=true#packetery_customs_declaration_metabox" ); $this->latteEngine->render( PACKETERY_PLUGIN_DIR . '/template/order/customs-declaration-metabox.latte', [ 'form' => $form, 'hasInvoiceFile' => $hasInvoiceFile, 'hasEadFile' => $hasEadFile, 'runWizardUrl' => $runWizardUrl, 'translations' => [ 'addCustomsDeclarationItem' => $this->wpAdapter->__( 'Add item', 'packeta' ), 'delete' => $this->wpAdapter->__( 'Delete', 'packeta' ), 'itemsLabel' => $this->wpAdapter->__( 'Items', 'packeta' ), 'fileUploaded' => $this->wpAdapter->__( 'File uploaded.', 'packeta' ), 'runWizard' => $this->wpAdapter->__( 'Run customs declaration wizard', 'packeta' ), ], ] ); } /** * Creates form. * * @param array $structureData Data specifying how form will be constructed. * When user browser sends added customs declaration items, the form factory has to reflect that. * @param Entity\CustomsDeclaration|null $customsDeclaration Related customs declaration. * * @return Form */ private function createForm( array $structureData, ?Entity\CustomsDeclaration $customsDeclaration ): Form { $form = $this->formFactory->create(); $activator = $form->addCheckbox( self::FORM_ACTIVATOR_NAME, __( 'View/hide customs declaration form', 'packeta' ) ); if ( $this->request->getQuery( 'wizard-order-detail-custom-declaration-enabled' ) === 'true' ) { $activator->setValue( true ); } $activator ->addCondition( Form::FILLED ) ->toggle( 'customs-declaration-container' ); $prefixContainer = $form->addContainer( self::FORM_CONTAINER_NAME ); $ead = $prefixContainer->addSelect( 'ead', __( 'EAD', 'packeta' ), [ self::EAD_OWN => __( 'Self-declaration (I have a EAD)', 'packeta' ), self::EAD_CREATE => __( 'Issuing a EAD via Packeta', 'packeta' ), self::EAD_CARRIER => __( 'Postal clearance (no EAD and no fees)', 'packeta' ), ] ); $ead ->addConditionOn( $activator, Form::FILLED ) ->setRequired(); $prefixContainer->addText( 'delivery_cost', __( 'Delivery cost', 'packeta' ) ) ->addConditionOn( $activator, Form::FILLED ) ->setRequired() ->addRule( Form::FLOAT ) ->addRule( ...FormRules::getGreaterThanParameters( 0 ) ); $prefixContainer->addText( 'invoice_number', __( 'Invoice number', 'packeta' ) ) ->addConditionOn( $activator, Form::FILLED ) ->setRequired(); $prefixContainer->addText( 'invoice_issue_date', __( 'Invoice issue date', 'packeta' ) ) ->addConditionOn( $activator, Form::FILLED ) ->setRequired() ->addRule( ...FormRules::getDateParameters() ); $invoiceFile = $prefixContainer->addUpload( 'invoice_file', __( 'Invoice PDF file', 'packeta' ) ) ->setRequired( false ); if ( $customsDeclaration === null || $customsDeclaration->hasInvoiceFileContent() === false ) { $invoiceFile ->addConditionOn( $activator, Form::FILLED ) ->addConditionOn( $ead, Form::EQUAL, self::EAD_OWN ) ->setRequired() ->endCondition() ->addConditionOn( $ead, Form::EQUAL, self::EAD_CREATE ) ->setRequired(); } $prefixContainer->addText( 'mrn', __( 'MRN', 'packeta' ) ) ->setRequired( false ) ->addConditionOn( $activator, Form::FILLED ) ->addRule( Form::MAX_LENGTH, null, 32 ) ->addConditionOn( $ead, Form::EQUAL, self::EAD_OWN ) ->toggle( 'customs-declaration-own-field-mrn' ) ->setRequired(); $eadFile = $prefixContainer->addUpload( 'ead_file', __( 'EAD PDF file', 'packeta' ) ) ->setRequired( false ) ->addConditionOn( $ead, Form::EQUAL, self::EAD_OWN ) ->toggle( 'customs-declaration-own-field-ead_file' ); if ( $customsDeclaration === null || $customsDeclaration->hasEadFileContent() === false ) { $eadFile ->addConditionOn( $activator, Form::FILLED ) ->addConditionOn( $ead, Form::EQUAL, self::EAD_OWN ) ->setRequired(); } $form->addSubmit( 'save' ); $items = $prefixContainer->addContainer( 'items' ); $itemsData = $structureData[ self::FORM_CONTAINER_NAME ]['items'] ?? null; if ( $itemsData === null ) { $this->addCustomsDeclarationItem( $activator, $items, 'new_0' ); } else { foreach ( $itemsData as $itemId => $itemDefaults ) { $this->addCustomsDeclarationItem( $activator, $items, (string) $itemId ); } } $form->onSuccess[] = [ $this, 'onFormSuccess' ]; $form->onError[] = [ $this, 'onFormError' ]; return $form; } /** * On form error. * * @param Form $form Form. * @return void */ public function onFormError( Form $form ): void { /** Form input control. @var BaseControl[] $controls */ $controls = $form->getComponents( true, BaseControl::class ); foreach ( $controls as $control ) { if ( $control instanceof BaseControl ) { foreach ( $control->getErrors() as $error ) { $this->messageManager->flashMessageObject( Message::create() ->setText( sprintf( '%s: %s', $control->getCaption(), $error ) ) ->setType( MessageManager::TYPE_ERROR ) ); } } } } /** * On form success callback. * * @param Form $form Form. * @return void */ public function onFormSuccess( Form $form ): void { $order = $this->detailCommonLogic->getOrder(); if ( $order === null ) { return; } $fieldsToOmit = []; /** @var Container $customsDeclarationContainer */ $customsDeclarationContainer = $form[ self::FORM_CONTAINER_NAME ]; $prefixedValues = $form->getValues( 'array' ); $containerValues = $prefixedValues[ self::FORM_CONTAINER_NAME ]; $items = $containerValues['items']; unset( $containerValues['items'] ); if ( $prefixedValues[ self::FORM_ACTIVATOR_NAME ] === false ) { return; } $this->processUploadedFile( 'invoice_file', 'invoice_file_id', $containerValues, $customsDeclarationContainer, $fieldsToOmit ); $this->processUploadedFile( 'ead_file', 'ead_file_id', $containerValues, $customsDeclarationContainer, $fieldsToOmit ); if ( $containerValues['mrn'] === '' ) { $containerValues['mrn'] = null; } $containerValues['id'] = null; $oldCustomsDeclaration = $this->customsDeclarationRepository->getByOrderNumber( $order->getNumber() ); if ( $oldCustomsDeclaration !== null ) { $containerValues['id'] = $oldCustomsDeclaration->getId(); } $customsDeclaration = $this->customsDeclarationEntityFactory->fromStandardizedStructure( $containerValues, $order->getNumber() ); $customsDeclaration->setInvoiceFile( $containerValues['invoice_file'], (bool) $containerValues['invoice_file'] ); $customsDeclaration->setEadFile( $containerValues['ead_file'], (bool) $containerValues['ead_file'] ); $updatedRowCount = $this->customsDeclarationRepository->save( $customsDeclaration, $fieldsToOmit ); if ( $updatedRowCount === false ) { $this->messageManager->flash_message( (string) $this->wpAdapter->__( 'An error occurred while saving the customs declaration. More details in WC log.', 'packeta' ), MessageManager::TYPE_ERROR ); } $customsDeclarationItems = $this->customsDeclarationRepository->getItemsByCustomsDeclarationId( $customsDeclaration->getId() ); $customsDeclaration->setItems( $customsDeclarationItems ); foreach ( $customsDeclarationItems as $customsDeclarationItem ) { $itemId = $customsDeclarationItem->getId(); if ( ! isset( $items[ $itemId ] ) ) { try { $this->customsDeclarationRepository->deleteItem( (int) $itemId ); } catch ( DeleteErrorException $e ) { // No user message needed. } } } $itemSavingError = false; foreach ( $items as $itemId => $item ) { if ( strpos( (string) $itemId, 'new_' ) === 0 ) { $itemId = null; } else { $itemId = (string) $itemId; } $item['id'] = $itemId; $item['customs_declaration_id'] = $customsDeclaration->getId(); $updatedRowCount = $this->customsDeclarationRepository->saveItem( $this->customsDeclarationEntityFactory->createItemFromStandardizedStructure( $item ) ); if ( $updatedRowCount === false ) { $itemSavingError = true; } } if ( $itemSavingError === true ) { $this->messageManager->flash_message( (string) $this->wpAdapter->__( 'An error occurred while saving the customs declaration items. More details in WC log.', 'packeta' ), MessageManager::TYPE_ERROR ); } } /** * Adds customs declaration item. * * @param Checkbox $activator Activating checkbox. * @param Container $container Container. * @param string $index Item index. * * @return void */ public function addCustomsDeclarationItem( Checkbox $activator, Container $container, string $index ): void { $item = $container->addContainer( $index ); $item->addText( 'customs_code', __( 'Customs code', 'packeta' ) ) ->addConditionOn( $activator, Form::FILLED ) ->setRequired() ->addRule( Form::MAX_LENGTH, null, 8 ); $item->addText( 'value', __( 'Value', 'packeta' ) ) ->addConditionOn( $activator, Form::FILLED ) ->setRequired() ->addRule( Form::FLOAT ) ->addRule( ...FormRules::getGreaterThanParameters( 0 ) ); $item->addText( 'product_name_en', __( 'Product name (EN)', 'packeta' ) ) ->addConditionOn( $activator, Form::FILLED ) ->setRequired(); $item->addText( 'product_name', __( 'Product name', 'packeta' ) ); $item->addText( 'units_count', __( 'Units count', 'packeta' ) ) ->addConditionOn( $activator, Form::FILLED ) ->setRequired() ->addRule( Form::INTEGER ) ->addRule( ...FormRules::getGreaterThanParameters( 0 ) ); $item->addText( 'country_of_origin', __( 'Country of origin code', 'packeta' ) ) ->addConditionOn( $activator, Form::FILLED ) ->setRequired() ->addRule( Form::LENGTH, null, 2 ); $item->addText( 'weight', __( 'Weight (kg)', 'packeta' ) ) ->addConditionOn( $activator, Form::FILLED ) ->setRequired() ->addRule( Form::FLOAT ) ->addRule( ...FormRules::getGreaterThanParameters( 0 ) ) ->addFilter( static function ( float $value ): float { return CoreHelper::simplifyWeight( $value ); } ) ->addRule( ...FormRules::getGreaterThanParameters( 0 ) ); $item->addCheckbox( 'is_food_or_book', __( 'Food or book?', 'packeta' ) ); $item->addCheckbox( 'is_voc', __( 'Is VOC?', 'packeta' ) ); } /** * Handle file upload. * * @param string $key File key. * @param string $relatedFileIdKey Related file id key. * @param array $containerValues Container values. * @param Container $formContainer Form container. * @param string[] $fieldsToOmit Fields to omit. * * @return void */ private function processUploadedFile( string $key, string $relatedFileIdKey, array &$containerValues, Container $formContainer, array &$fieldsToOmit ): void { $fileUpload = $containerValues[ $key ]; $uploadControl = $formContainer[ $key ]; if ( $uploadControl instanceof UploadControl ) { if ( $fileUpload->hasFile() && $fileUpload->getSize() <= 0 ) { $uploadControl->addError( __( 'Uploaded file is empty.', 'packeta' ) ); $fileUpload = new FileUpload( null ); } if ( $fileUpload->hasFile() && self::MAX_UPLOAD_FILE_MEGABYTES * 1024 * 1024 === $fileUpload->getSize() ) { // translators: %d is numeric value. $uploadControl->addError( sprintf( __( 'Uploaded file is too big for storage. Max size is %d MB.', 'packeta' ), self::MAX_UPLOAD_FILE_MEGABYTES ) ); $fileUpload = new FileUpload( null ); } if ( $fileUpload->hasFile() && $fileUpload->isOk() ) { $containerValues[ $key ] = static function () use ( $fileUpload ): string { return $fileUpload->getContents(); }; $containerValues[ $relatedFileIdKey ] = null; } if ( $fileUpload->hasFile() && $fileUpload->isOk() === false ) { $containerValues[ $key ] = null; $containerValues[ $relatedFileIdKey ] = null; $uploadControl->addError( __( 'File failed to upload.', 'packeta' ) ); } if ( $containerValues[ $key ] instanceof FileUpload ) { $containerValues[ $key ] = null; $containerValues[ $relatedFileIdKey ] = null; $fieldsToOmit[] = $key; $fieldsToOmit[] = $relatedFileIdKey; } } } }