--- a/formgent/app/DTO/AllResponsesReadDTO.php
+++ b/formgent/app/DTO/AllResponsesReadDTO.php
@@ -13,6 +13,8 @@
private ?int $is_read = null;
+ private ?int $is_starred = null;
+
private ?int $is_completed = null;
private ?string $search = null;
@@ -92,6 +94,28 @@
return $this;
}
+
+ /**
+ * Get the value of is_starred
+ *
+ * @return ?int
+ */
+ public function get_is_starred(): ?int {
+ return $this->is_starred;
+ }
+
+ /**
+ * Set the value of is_starred
+ *
+ * @param ?int $is_starred
+ *
+ * @return self
+ */
+ public function set_is_starred( ?int $is_starred ): self {
+ $this->is_starred = $is_starred;
+
+ return $this;
+ }
/**
* Get the value of is_completed
--- a/formgent/app/DTO/FormReadDTO.php
+++ b/formgent/app/DTO/FormReadDTO.php
@@ -21,6 +21,8 @@
private ?string $type = null;
+ private ?string $status = null;
+
/**
* Get the value of page
*
@@ -174,4 +176,26 @@
return $this;
}
+
+ /**
+ * Get the value of status
+ *
+ * @return ?string
+ */
+ public function get_status(): ?string {
+ return $this->status;
+ }
+
+ /**
+ * Set the value of status
+ *
+ * @param ?string $status
+ *
+ * @return self
+ */
+ public function set_status( ?string $status ): self {
+ $this->status = $status;
+
+ return $this;
+ }
}
No newline at end of file
--- a/formgent/app/DTO/PayDTO.php
+++ b/formgent/app/DTO/PayDTO.php
@@ -6,7 +6,8 @@
use FormGentWpMVCDTODTO;
-class PayDTO extends DTO {
+class PayDTO extends DTO
+{
public OrderDTO $order;
public PaymentDTO $payment;
@@ -16,6 +17,18 @@
*/
public array $order_items;
+ /** Runtime payment metadata (not persisted). */
+ public array $meta = [];
+
+ public function get_meta(): array {
+ return $this->meta;
+ }
+
+ public function set_meta( array $meta ): self {
+ $this->meta = $meta;
+ return $this;
+ }
+
/**
* Get the value of order
*
@@ -28,7 +41,7 @@
/**
* Set the value of order
*
- * @param OrderDTO $order
+ * @param OrderDTO $order
*
* @return self
*/
@@ -50,7 +63,7 @@
/**
* Set the value of payment
*
- * @param PaymentDTO $payment
+ * @param PaymentDTO $payment
*
* @return self
*/
@@ -72,7 +85,7 @@
/**
* Set the value of order_items
*
- * @param OrderItemDTO[] $order_items
+ * @param OrderItemDTO[] $order_items
*
* @return self
*/
--- a/formgent/app/DTO/ResponseLogDTO.php
+++ b/formgent/app/DTO/ResponseLogDTO.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace FormGentAppDTO;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentWpMVCDTODTO;
+
+class ResponseLogDTO extends DTO {
+ private int $id;
+
+ private int $response_id;
+
+ private string $action = 'entry_edited';
+
+ private ?int $created_by = null;
+
+ private ?string $meta = null;
+
+ /**
+ * Get the value of id
+ *
+ * @return int
+ */
+ public function get_id() {
+ return $this->id;
+ }
+
+ /**
+ * Set the value of id
+ *
+ * @param int $id
+ *
+ * @return self
+ */
+ public function set_id( int $id ) {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ /**
+ * Get the value of response_id
+ *
+ * @return int
+ */
+ public function get_response_id() {
+ return $this->response_id;
+ }
+
+ /**
+ * Set the value of response_id
+ *
+ * @param int $response_id
+ *
+ * @return self
+ */
+ public function set_response_id( int $response_id ) {
+ $this->response_id = $response_id;
+
+ return $this;
+ }
+
+ /**
+ * Get the value of action
+ *
+ * @return string
+ */
+ public function get_action() {
+ return $this->action;
+ }
+
+ /**
+ * Set the value of action
+ *
+ * @param string $action
+ *
+ * @return self
+ */
+ public function set_action( string $action ) {
+ $this->action = $action;
+
+ return $this;
+ }
+
+ /**
+ * Get the value of created_by
+ *
+ * @return int|null
+ */
+ public function get_created_by() {
+ return $this->created_by;
+ }
+
+ /**
+ * Set the value of created_by
+ *
+ * @param int|null $created_by
+ *
+ * @return self
+ */
+ public function set_created_by( ?int $created_by ) {
+ $this->created_by = $created_by;
+
+ return $this;
+ }
+
+ /**
+ * Get the value of meta
+ *
+ * @return string|null
+ */
+ public function get_meta() {
+ return $this->meta;
+ }
+
+ /**
+ * Set the value of meta
+ *
+ * @param string|null $meta
+ *
+ * @return self
+ */
+ public function set_meta( ?string $meta ) {
+ $this->meta = $meta;
+
+ return $this;
+ }
+}
--- a/formgent/app/DTO/ResponseLogReadDTO.php
+++ b/formgent/app/DTO/ResponseLogReadDTO.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace FormGentAppDTO;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentWpMVCDTODTO;
+
+class ResponseLogReadDTO extends DTO {
+ private int $response_id;
+
+ private int $page = 1;
+
+ private int $per_page = 10;
+
+ /**
+ * Get the value of response_id
+ *
+ * @return int
+ */
+ public function get_response_id() {
+ return $this->response_id;
+ }
+
+ /**
+ * Set the value of response_id
+ *
+ * @param int $response_id
+ *
+ * @return self
+ */
+ public function set_response_id( int $response_id ) {
+ $this->response_id = $response_id;
+
+ return $this;
+ }
+
+ /**
+ * Get the value of page
+ *
+ * @return int
+ */
+ public function get_page() {
+ return $this->page;
+ }
+
+ /**
+ * Set the value of page
+ *
+ * @param int $page
+ *
+ * @return self
+ */
+ public function set_page( int $page ) {
+ $this->page = $page;
+
+ return $this;
+ }
+
+ /**
+ * Get the value of per_page
+ *
+ * @return int
+ */
+ public function get_per_page() {
+ return $this->per_page;
+ }
+
+ /**
+ * Set the value of per_page
+ *
+ * @param int $per_page
+ *
+ * @return self
+ */
+ public function set_per_page( int $per_page ) {
+ $this->per_page = $per_page;
+
+ return $this;
+ }
+}
--- a/formgent/app/DTO/ResponseReadDTO.php
+++ b/formgent/app/DTO/ResponseReadDTO.php
@@ -15,7 +15,9 @@
private ?int $is_read = null;
- private int $is_completed = 0;
+ private ?int $is_starred = null;
+
+ private ?int $is_completed = null;
private ?string $search = null;
@@ -118,22 +120,44 @@
}
/**
+ * Get the value of is_starred
+ *
+ * @return ?int
+ */
+ public function get_is_starred(): ?int {
+ return $this->is_starred;
+ }
+
+ /**
+ * Set the value of is_starred
+ *
+ * @param ?int $is_starred
+ *
+ * @return self
+ */
+ public function set_is_starred( ?int $is_starred ): self {
+ $this->is_starred = $is_starred;
+
+ return $this;
+ }
+
+ /**
* Get the value of is_completed
*
- * @return int
+ * @return ?int
*/
- public function get_is_completed(): int {
+ public function get_is_completed(): ?int {
return $this->is_completed;
}
/**
* Set the value of is_completed
*
- * @param int $is_completed
+ * @param ?int $is_completed
*
* @return self
*/
- public function set_is_completed( int $is_completed ): self {
+ public function set_is_completed( ?int $is_completed ): self {
$this->is_completed = $is_completed;
return $this;
--- a/formgent/app/Helpers/Form.php
+++ b/formgent/app/Helpers/Form.php
@@ -62,11 +62,31 @@
* @return void
*/
protected function remove_labels( array &$attributes ): void {
+ // Keep a minimal representation of choice options for frontend runtime features
+ // (e.g. calculations using numeric_value). Labels are stripped to keep payload small.
+ if ( isset( $attributes['options'] ) && is_array( $attributes['options'] ) ) {
+ $attributes['options'] = array_map(
+ static function( $opt ) {
+ if ( ! is_array( $opt ) ) {
+ return $opt;
+ }
+
+ $keep = [];
+ foreach ( [ 'value', 'numeric_value', 'price', 'is_default', 'is_other' ] as $k ) {
+ if ( array_key_exists( $k, $opt ) ) {
+ $keep[ $k ] = $opt[ $k ];
+ }
+ }
+ return $keep;
+ },
+ $attributes['options']
+ );
+ }
+
foreach ( [
'label',
'sub_label',
'description',
- 'options',
'button_text',
'placeholder',
'date_placeholder',
--- a/formgent/app/Helpers/form-helpers.php
+++ b/formgent/app/Helpers/form-helpers.php
@@ -75,13 +75,14 @@
function formgent_form_default_values_functions() {
return apply_filters(
- 'formgent_default_values_functions', [
+ 'formgent_default_values_functions',
+ [
'ip' => 'formgent_get_user_ip_address',
'site_url' => 'site_url',
- 'site_title' => function() {
+ 'site_title' => function () {
return get_bloginfo( 'name' );
},
- 'user' => function( $property ) {
+ 'user' => function ( $property ) {
// For non-logged-in users, keep user-based defaults blank.
if ( ! is_user_logged_in() ) {
return '';
@@ -155,26 +156,26 @@
$base = array_shift( $parts );
// Cache resolved values per token so repeated placeholders are cheap.
- if ( ! isset( $dynamic_values[ $token ] ) ) {
+ if ( ! isset( $dynamic_values[$token] ) ) {
$dynamic_value = '';
- if ( isset( $values_functions[ $base ] ) && is_callable( $values_functions[ $base ] ) ) {
+ if ( isset( $values_functions[$base] ) && is_callable( $values_functions[$base] ) ) {
// Resolve the user property if applicable.
if ( 'user' === $base && ! empty( $parts ) ) {
$property = implode( '.', $parts );
- $dynamic_value = $values_functions[ $base ]( $property );
+ $dynamic_value = $values_functions[$base]( $property );
} else {
- $dynamic_value = $values_functions[ $base ]();
+ $dynamic_value = $values_functions[$base]();
}
}
- $dynamic_values[ $token ] = $dynamic_value;
+ $dynamic_values[$token] = $dynamic_value;
}
// Replace the placeholder with the resolved value.
- $placeholder = $raw_placeholders[ $match_index ] ?? '';
+ $placeholder = $raw_placeholders[$match_index] ?? '';
if ( '' !== $placeholder ) {
- $value = str_replace( $placeholder, $dynamic_values[ $token ], $value );
+ $value = str_replace( $placeholder, $dynamic_values[$token], $value );
}
}
@@ -269,15 +270,15 @@
$flattened_answers = [];
foreach ( $answers as $answer ) {
- if ( ! isset( $fields[ $answer->field_name ] ) ) {
+ if ( ! isset( $fields[$answer->field_name] ) ) {
continue;
}
- $form_field = $fields[ $answer->field_name ];
+ $form_field = $fields[$answer->field_name];
$field_dto = formgent_make_answer_field_dto( $answer, $form_field );
- $flattened_answers[ $field_dto->get_field_id() ] = $field_dto;
+ $flattened_answers[$field_dto->get_field_id()] = $field_dto;
if ( empty( $answer->children ) ) {
continue;
@@ -328,8 +329,8 @@
$embed_post = get_post();
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] )
- ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
- : '';
+ ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) )
+ : '';
$browser = '';
$platform = '';
@@ -366,6 +367,7 @@
$preset = [
'site_title' => get_option( 'blogname', '' ),
+ 'site_name' => get_option( 'blogname', '' ),
'site_url' => esc_url_raw( site_url() ),
'form_title' => $form_post ? $form_post->post_title : '',
'browser_name' => $browser,
@@ -380,6 +382,7 @@
$preset += [
'user_id' => (string) $current_user->ID,
'user_display_name' => $current_user->display_name,
+ 'user_name' => $current_user->display_name,
'user_first_name' => $current_user->first_name,
'user_last_name' => $current_user->last_name,
'user_email' => $current_user->user_email,
@@ -393,7 +396,7 @@
$cookies = [];
foreach ( $_COOKIE as $key => $value ) {
- $cookies[ $key ] = is_string( $value ) ? sanitize_text_field( wp_unslash( $value ) ) : $value;
+ $cookies[$key] = is_string( $value ) ? sanitize_text_field( wp_unslash( $value ) ) : $value;
}
$preset += [
@@ -439,13 +442,13 @@
$replacements = [];
foreach ( $matches[1] as $index => $raw_token ) {
- $placeholder = $matches[0][ $index ];
+ $placeholder = $matches[0][$index];
$token = trim( $raw_token );
$token_key = strtolower( $token );
// 1. Preset tags (user/site/admin)
- if ( isset( $preset_values[ $token_key ] ) ) {
- $replacements[ $placeholder ] = esc_html( $preset_values[ $token_key ] );
+ if ( isset( $preset_values[$token_key] ) ) {
+ $replacements[$placeholder] = esc_html( $preset_values[$token_key] );
continue;
}
@@ -462,7 +465,7 @@
$resolved = $resolver->resolve_from_form_data( $form_data, $form_fields, $field_reference );
if ( null !== $resolved ) {
- $replacements[ $placeholder ] = esc_html( $resolved );
+ $replacements[$placeholder] = esc_html( $resolved );
continue;
}
}
@@ -475,7 +478,7 @@
$resolved = $resolver->resolve_from_answers( $answers, $form_fields, $field_reference );
if ( null !== $resolved ) {
- $replacements[ $placeholder ] = esc_html( $resolved );
+ $replacements[$placeholder] = esc_html( $resolved );
continue;
}
}
@@ -489,15 +492,49 @@
$answers = $answers ?? formgent_get_form_answers( $form_id, $response_id, true );
if ( $response_dto ) {
- $resolved = $preset_repo->transform_value( '{{' . $raw_token . '}}', $answers, $response_dto, '' );
- $replacements[ $placeholder ] = esc_html( (string) $resolved );
+ $resolved = $preset_repo->transform_value( '{{' . $raw_token . '}}', $answers, $response_dto, '' );
+ $replacements[$placeholder] = esc_html( (string) $resolved );
continue;
}
}
// 4. Fallback: leave untouched so frontend can handle it later
- $replacements[ $placeholder ] = $placeholder;
+ $replacements[$placeholder] = $placeholder;
}
return wp_kses_post( strtr( $html_content, $replacements ) );
+}
+
+/**
+ * Replace save-resume-specific placeholders in a string.
+ *
+ * Supported tokens (double-curly-brace style):
+ * {{resume_url}} – the unique resume URL with the token.
+ * {{form_title}} – the form's post_title.
+ * {{site_name}} – get_bloginfo('name').
+ * {{save_email}} – the email address the user entered.
+ *
+ * This helper is intentionally standalone: it does NOT depend on ResponseDTO
+ * or any response data, making it safe to call for transactional save-resume
+ * emails where no submission has been created yet.
+ *
+ * @param string $content Raw content (HTML or plain text) with {{token}} placeholders.
+ * @param array $replacements Associative array of token_key => replacement_value.
+ *
+ * @return string
+ */
+function formgent_replace_save_resume_placeholders( string $content, array $replacements ): string {
+ if ( '' === $content || false === strpos( $content, '{{' ) ) {
+ return $content;
+ }
+
+ $search = [];
+ $replace = [];
+
+ foreach ( $replacements as $key => $value ) {
+ $search[] = '{{' . $key . '}}';
+ $replace[] = (string) $value;
+ }
+
+ return str_replace( $search, $replace, $content );
}
No newline at end of file
--- a/formgent/app/Helpers/payment.php
+++ b/formgent/app/Helpers/payment.php
@@ -26,11 +26,11 @@
function formgent_payment_processor( $payment_gateway ): PaymentInterface {
$payment_gateways = formgent_get_payment_gateways();
- if ( ! isset( $payment_gateways[ $payment_gateway ] ) ) {
+ if ( ! isset( $payment_gateways[$payment_gateway] ) ) {
throw new Exception( esc_html__( "Payment gateway not found.", 'formgent' ) );
}
- return formgent_singleton( $payment_gateways[ $payment_gateway ]['processor'] );
+ return formgent_singleton( $payment_gateways[$payment_gateway]['processor'] );
}
function formgent_is_payment_form( array $fields_data ) {
@@ -38,7 +38,7 @@
$payment_field_types = array_keys( $payment_gateways );
// Add other payment-related field types
- $payment_field_types = array_merge( $payment_field_types, ['custom-payment-amount', 'payment-item', 'quantity'] );
+ $payment_field_types = array_merge( $payment_field_types, ['custom-payment-amount', 'payment-item', 'quantity', 'payment'] );
foreach ( $fields_data as $field_data ) {
if ( in_array( $field_data['field_type'], $payment_field_types ) ) {
@@ -453,7 +453,8 @@
function formgent_price( $price, array $args = [] ) {
$args = wp_parse_args(
- $args, [
+ $args,
+ [
'currency' => 'USD'
]
);
@@ -463,4 +464,5 @@
$price_format = formgent_get_price_format();
return sprintf( $price_format, $currency_symbol, $price );
-};
No newline at end of file
+}
+;
No newline at end of file
--- a/formgent/app/Http/Controllers/Admin/FormController.php
+++ b/formgent/app/Http/Controllers/Admin/FormController.php
@@ -34,7 +34,8 @@
'sort_by' => 'string|accepted:last_modified,date_created,alphabetical,last_submission,unread,draft,publish',
'date_type' => 'string|accepted:all,today,yesterday,last_week,last_month,date_frame',
'date_frame' => 'array',
- 'type' => 'string|accepted:all,general,conversational'
+ 'type' => 'string|accepted:all,general,conversational',
+ 'status' => 'string|accepted:all,publish,draft'
]
);
@@ -48,6 +49,7 @@
$dto->set_date_type( $wp_rest_request->get_param( 'date_type' ) );
$dto->set_date_frame( (array) $wp_rest_request->get_param( 'date_frame' ) );
$dto->set_type( $wp_rest_request->get_param( 'type' ) );
+ $dto->set_status( $wp_rest_request->get_param( 'status' ) );
$data = $this->form_repository->get( $dto );
$response = $this->pagination( $wp_rest_request, $data['total'], $dto->get_per_page() );
--- a/formgent/app/Http/Controllers/Admin/ResponseController.php
+++ b/formgent/app/Http/Controllers/Admin/ResponseController.php
@@ -46,10 +46,11 @@
's' => 'string|max:255',
'form_id' => 'numeric',
'is_read' => 'numeric|accepted:0,1',
+ 'is_starred' => 'numeric|accepted:0,1',
'order_by' => 'string|max:50',
'order' => 'string|accepted:asc,desc',
'order_field_type' => 'string|accepted:response,answer',
- 'is_completed' => 'required|numeric|accepted:0,1',
+ 'is_completed' => 'numeric|accepted:0,1',
'date_type' => 'string|accepted:all,today,yesterday,last_week,last_month,date_frame',
'date_frame' => 'array',
]
@@ -68,10 +69,20 @@
$dto->set_is_read( $wp_rest_request->get_param( 'is_read' ) );
}
+ if ( $wp_rest_request->has_param( 'is_starred' ) ) {
+ $dto->set_is_starred( $wp_rest_request->get_param( 'is_starred' ) );
+ }
+
$dto->set_order( $wp_rest_request->get_param( 'order' ) ?? 'desc' )
->set_order_by( $wp_rest_request->get_param( 'order_by' ) ?? 'id' )
- ->set_order_field_type( $wp_rest_request->get_param( 'order_field_type' ) ?? 'response' )
- ->set_is_completed( $wp_rest_request->get_param( 'is_completed' ) );
+ ->set_order_field_type( $wp_rest_request->get_param( 'order_field_type' ) ?? 'response' );
+
+ if ( $wp_rest_request->has_param( 'is_completed' ) ) {
+ $value = $wp_rest_request->get_param( 'is_completed' );
+ if ( $value !== null && $value !== '' ) {
+ $dto->set_is_completed( (int) $value );
+ }
+ }
// Set date filtering
$dto->set_date_type( $wp_rest_request->get_param( 'date_type' ) );
@@ -113,6 +124,7 @@
'page' => 'numeric',
's' => 'string|max:255',
'is_read' => 'numeric|accepted:0,1',
+ 'is_starred' => 'numeric|accepted:0,1',
'order_by' => 'string|max:50',
'order' => 'string|accepted:asc,desc',
'is_completed' => 'numeric|accepted:0,1',
@@ -140,6 +152,10 @@
$dto->set_is_read( $wp_rest_request->get_param( 'is_read' ) );
}
+ if ( $wp_rest_request->has_param( 'is_starred' ) ) {
+ $dto->set_is_starred( $wp_rest_request->get_param( 'is_starred' ) );
+ }
+
if ( $wp_rest_request->has_param( 'is_completed' ) ) {
$dto->set_is_completed( $wp_rest_request->get_param( 'is_completed' ) );
}
@@ -246,9 +262,12 @@
$dto->set_form_id( intval( $form_id_param ) );
}
- // When fetching by id without form_id (e.g. deep link or very old response not in list),
- // look up the response to get form_id and is_completed
- if ( $dto->get_id() !== null && ( ! $dto->get_form_id() || $dto->get_form_id() === 0 ) ) {
+ // When fetching by id, always look up the response to get canonical form_id and is_completed.
+ // This prevents stale client-side filters (e.g. is_completed=0) from hiding the response
+ // after it transitions to completed.
+ $response_by_id = null;
+
+ if ( $dto->get_id() !== null ) {
$response_by_id = $this->repository->get_by_id( $dto->get_id() );
if ( ! $response_by_id ) {
return Response::send(
@@ -260,13 +279,15 @@
}
$dto->set_form_id( (int) $response_by_id->form_id );
$dto->set_is_completed( (int) $response_by_id->is_completed );
+ $dto->set_is_read( (int) $response_by_id->is_read );
} elseif ( $wp_rest_request->has_param( 'is_completed' ) ) {
- $dto->set_is_completed( (int) $wp_rest_request->get_param( 'is_completed' ) );
- } else {
- $dto->set_is_completed( 1 );
+ $value = $wp_rest_request->get_param( 'is_completed' );
+ if ( $value !== null && $value !== '' ) {
+ $dto->set_is_completed( (int) $value );
+ }
}
- if ( $wp_rest_request->has_param( 'is_read' ) ) {
+ if ( $dto->get_id() === null && $wp_rest_request->has_param( 'is_read' ) ) {
$dto->set_is_read( $wp_rest_request->get_param( 'is_read' ) );
}
--- a/formgent/app/Http/Controllers/Admin/ResponseLogController.php
+++ b/formgent/app/Http/Controllers/Admin/ResponseLogController.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace FormGentAppHttpControllersAdmin;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentAppDTOResponseLogReadDTO;
+use FormGentAppRepositoriesResponseLogRepository;
+use FormGentWpMVCRoutingResponse;
+use FormGentAppHttpControllersController;
+use FormGentWpMVCRequestValidatorValidator;
+use WP_REST_Request;
+
+class ResponseLogController extends Controller {
+ public ResponseLogRepository $repository;
+
+ public function __construct( ResponseLogRepository $repository ) {
+ $this->repository = $repository;
+ }
+
+ public function index( Validator $validate, WP_REST_Request $request ) {
+ $validate->validate(
+ [
+ 'response_id' => 'required|numeric',
+ ]
+ );
+
+ $per_page = min( 100, max( 1, intval( $request->get_param( 'per_page' ) ?: 10 ) ) );
+
+ $dto = ( new ResponseLogReadDTO )
+ ->set_response_id( intval( $request->get_param( 'response_id' ) ) )
+ ->set_page( intval( $request->get_param( 'page' ) ?: 1 ) )
+ ->set_per_page( $per_page );
+
+ $result = $this->repository->get_paginated( $dto );
+
+ return Response::send(
+ array_merge(
+ [ 'logs' => $result['logs'] ],
+ $this->pagination( $request, $result['total'], $dto->get_per_page() )
+ )
+ );
+ }
+
+ public function delete( Validator $validate, WP_REST_Request $request ) {
+ $validate->validate(
+ [
+ 'id' => 'required|numeric',
+ 'response_id' => 'required|numeric',
+ ]
+ );
+
+ $this->repository->delete(
+ intval( $request->get_param( 'response_id' ) ),
+ intval( $request->get_param( 'id' ) )
+ );
+
+ return Response::send(
+ [
+ 'message' => esc_html__( 'Log deleted successfully.', 'formgent' ),
+ ]
+ );
+ }
+}
--- a/formgent/app/Http/Controllers/Admin/SubscriptionController.php
+++ b/formgent/app/Http/Controllers/Admin/SubscriptionController.php
@@ -0,0 +1,203 @@
+<?php
+
+namespace FormGentAppHttpControllersAdmin;
+
+defined( "ABSPATH" ) || exit;
+
+use FormGentAppHttpControllersController;
+use FormGentAppRepositoriesOrderRepository;
+use FormGentWpMVCRoutingResponse;
+use FormGentWpMVCRequestValidatorValidator;
+use WP_REST_Request;
+
+class SubscriptionController extends Controller {
+ public OrderRepository $repository;
+
+ public function __construct( OrderRepository $repository ) {
+ $this->repository = $repository;
+ }
+
+ public function details( Validator $validator, WP_REST_Request $wp_rest_request ) {
+ $validator->validate(
+ [
+ 'response_id' => 'required|numeric',
+ 'order_id' => 'required|numeric',
+ ]
+ );
+
+ $response_id = intval( $wp_rest_request->get_param( 'response_id' ) );
+ $order_id = intval( $wp_rest_request->get_param( 'order_id' ) );
+
+ $order = $this->repository->first_by_response_id( $response_id, true );
+
+ if ( ! $order || (int) $order->id !== $order_id ) {
+ return Response::send(
+ [
+ 'message' => esc_html__( 'Order not found.', 'formgent' ),
+ ],
+ 404
+ );
+ }
+
+ $payment = $order->payment;
+
+ if ( ! $payment ) {
+ return Response::send(
+ [
+ 'message' => esc_html__( 'Payment not found.', 'formgent' ),
+ ],
+ 404
+ );
+ }
+
+ $response_repo = formgent_response_repository();
+ $meta_key = 'formgent_subscription_meta_' . $payment->id;
+ $subscription_meta = $response_repo->get_meta_value( $response_id, $meta_key );
+
+ if ( ! $subscription_meta ) {
+ return Response::send(
+ [
+ 'is_subscription' => false,
+ ]
+ );
+ }
+
+ $subscription_meta = json_decode( $subscription_meta, true );
+
+ if ( ! is_array( $subscription_meta ) ) {
+ return Response::send(
+ [
+ 'is_subscription' => false,
+ ]
+ );
+ }
+
+ $gateway = $subscription_meta['gateway'] ?? $payment->method;
+
+ // Build base details from stored meta.
+ $details = [
+ 'is_subscription' => true,
+ 'gateway' => $gateway,
+ 'subscription_id' => $payment->transaction_id ?? '',
+ 'plan_name' => $subscription_meta['plan_name'] ?? '',
+ 'billing_interval' => $subscription_meta['billing_interval'] ?? 'monthly',
+ 'status' => 'active',
+ 'start_date' => $payment->created_at ?? '',
+ 'next_billing_date' => '',
+ 'renewal_amount' => (float) ( $order->final_amount ?? 0 ),
+ 'currency' => $subscription_meta['currency'] ?? ( $payment->currency ?? 'USD' ),
+ 'trial_days' => (int) ( $subscription_meta['trial_days'] ?? 0 ),
+ 'trial_end' => null,
+ 'cancel_at' => null,
+ ];
+
+ // Let pro enrich with live gateway data.
+ $details = apply_filters(
+ 'formgent_admin_get_subscription_details',
+ $details,
+ $order,
+ $payment,
+ $subscription_meta
+ );
+
+ // Check for local cancel schedule.
+ $cancel_meta_key = 'formgent_subscription_cancel_' . $payment->id;
+ $cancel_meta = $response_repo->get_meta_value( $response_id, $cancel_meta_key );
+
+ if ( $cancel_meta ) {
+ $cancel_data = json_decode( $cancel_meta, true );
+ if ( is_array( $cancel_data ) && ! empty( $cancel_data['cancel_at'] ) ) {
+ $details['cancel_at'] = $cancel_data['cancel_at'];
+ if ( $details['status'] === 'active' ) {
+ $details['status'] = 'cancel_scheduled';
+ }
+ }
+ }
+
+ return Response::send( $details );
+ }
+
+ public function cancel( Validator $validator, WP_REST_Request $wp_rest_request ) {
+ $validator->validate(
+ [
+ 'response_id' => 'required|numeric',
+ 'order_id' => 'required|numeric',
+ ]
+ );
+
+ $response_id = intval( $wp_rest_request->get_param( 'response_id' ) );
+ $order_id = intval( $wp_rest_request->get_param( 'order_id' ) );
+
+ $order = $this->repository->first_by_response_id( $response_id, true );
+
+ if ( ! $order || (int) $order->id !== $order_id ) {
+ return Response::send(
+ [
+ 'message' => esc_html__( 'Order not found.', 'formgent' ),
+ ],
+ 404
+ );
+ }
+
+ $payment = $order->payment;
+
+ if ( ! $payment ) {
+ return Response::send(
+ [
+ 'message' => esc_html__( 'Payment not found.', 'formgent' ),
+ ],
+ 404
+ );
+ }
+
+ $response_repo = formgent_response_repository();
+ $meta_key = 'formgent_subscription_meta_' . $payment->id;
+ $subscription_meta = $response_repo->get_meta_value( $response_id, $meta_key );
+
+ if ( ! $subscription_meta ) {
+ return Response::send(
+ [
+ 'success' => false,
+ 'message' => esc_html__( 'No subscription found for this order.', 'formgent' ),
+ ],
+ 404
+ );
+ }
+
+ $subscription_meta = json_decode( $subscription_meta, true );
+
+ if ( ! is_array( $subscription_meta ) ) {
+ return Response::send(
+ [
+ 'success' => false,
+ 'message' => esc_html__( 'Invalid subscription metadata.', 'formgent' ),
+ ],
+ 422
+ );
+ }
+
+ $result = apply_filters(
+ 'formgent_admin_cancel_subscription',
+ null,
+ $order,
+ $payment,
+ $subscription_meta,
+ [
+ 'response_id' => $response_id,
+ 'cancel_type' => 'period_end',
+ ]
+ );
+
+ if ( $result === null ) {
+ return Response::send(
+ [
+ 'success' => false,
+ 'message' => esc_html__( 'Subscription cancellation is not supported for this gateway.', 'formgent' ),
+ ],
+ 422
+ );
+ }
+
+ return Response::send( $result );
+ }
+}
--- a/formgent/app/Http/Controllers/PaymentController.php
+++ b/formgent/app/Http/Controllers/PaymentController.php
@@ -15,7 +15,8 @@
use FormGentWpMVCRequestValidatorValidator;
use WP_REST_Request;
-class PaymentController extends Controller {
+class PaymentController extends Controller
+{
public function success( Validator $validator, WP_REST_Request $request ): array {
$validator->validate(
[
@@ -29,27 +30,27 @@
// Update payment status to paid
formgent_payment_repository()->update(
( new PaymentDTO )
- ->set_id( $payment_return_dto->get_payment_id() )
- ->set_transaction_id( $payment_return_dto->get_transaction_id() )
- ->set_billing_email( $payment_return_dto->get_billing_email() )
- ->set_billing_name( $payment_return_dto->get_billing_name() )
- ->set_billing_country( $payment_return_dto->get_billing_country() )
- ->set_status( PaymentStatus::PAID )
+ ->set_id( $payment_return_dto->get_payment_id() )
+ ->set_transaction_id( $payment_return_dto->get_transaction_id() )
+ ->set_billing_email( $payment_return_dto->get_billing_email() )
+ ->set_billing_name( $payment_return_dto->get_billing_name() )
+ ->set_billing_country( $payment_return_dto->get_billing_country() )
+ ->set_status( PaymentStatus::PAID )
);
$order_repository = formgent_order_repository();
// Update order status to paid
$order_repository->update(
- ( new OrderDTO )->set_id( $payment_return_dto->get_order_id() )->set_status( OrderStatus::PAID )
+ ( new OrderDTO )->set_id( $payment_return_dto->get_order_id() )->set_status( OrderStatus::PAID )
);
-
+
$order = $order_repository->get_by_id( $payment_return_dto->get_order_id() );
$settings = formgent_get_setting( 'payment' );
if ( ! empty( $settings['success_page'] ) ) {
$target_url = get_permalink( $settings['success_page'] );
-
+
if ( ! empty( $target_url ) ) {
$target_url = add_query_arg( 'order_id', $order->hash, $target_url );
wp_safe_redirect( $target_url, 301 );
@@ -113,7 +114,7 @@
'order_hash' => 'required|string',
]
);
-
+
$order_hash = $request->get_param( 'order_hash' );
$order_repository = formgent_order_repository();
$order = $order_repository->get_by( 'hash', $order_hash );
@@ -122,7 +123,8 @@
return Response::send(
[
'message' => esc_html__( 'Order not found.', 'formgent' )
- ], 404
+ ],
+ 404
);
}
@@ -130,7 +132,8 @@
return Response::send(
[
'message' => esc_html__( 'Order is already paid.', 'formgent' )
- ], 400
+ ],
+ 400
);
}
@@ -141,21 +144,23 @@
return Response::send(
[
'message' => esc_html__( 'Order items not found.', 'formgent' )
- ], 404
+ ],
+ 404
);
}
$payment_repository = formgent_payment_repository();
$last_payment = $payment_repository->get_by_order_id_last( $order->id );
$payment_method = $last_payment->method;
-
+
$payment_gateways = formgent_get_payment_gateways();
if ( ! isset( $payment_gateways[$payment_method] ) ) {
return Response::send(
[
'message' => esc_html__( 'Payment method not found.', 'formgent' )
- ], 404
+ ],
+ 404
);
}
@@ -181,7 +186,7 @@
$payment_dto = ( new PaymentDTO )
->set_order_id( $order_dto->get_id() )
->set_amount( $last_payment->amount )
- ->set_currency( $last_payment->amount )
+ ->set_currency( $order->currency )
->set_method( $payment_method );
$payment_dto->set_id( formgent_payment_repository()->create( $payment_dto ) );
--- a/formgent/app/Http/Controllers/ResponseController.php
+++ b/formgent/app/Http/Controllers/ResponseController.php
@@ -100,8 +100,17 @@
}
if ( ! empty( $validate_data['field_dtos'] ) ) {
+ // Save & Resume / partial-entry flows may have already stored draft answers
+ // against this response token. On final submit, replace existing answers
+ // to avoid duplicated rows in the completed entry.
+ try {
+ Answer::query()->where( 'response_id', $response->id )->delete();
+ } catch ( Throwable $e ) {
+ // If deletion fails for any reason, continue with insert to avoid blocking submit.
+ }
+
$this->answer_repository->creates( $response->id, $validate_data['field_dtos'] );
-
+
// Handle child fields if present.
if ( ! empty( $validate_data['parent_field_names'] ) ) {
$this->handle_child_fields( $response->id, $validate_data );
@@ -111,18 +120,18 @@
$this->repository->mark_as_completed( $response->id );
$response->is_completed = 1;
-
+
// Trigger the after response creation hook.
do_action( "formgent_after_create_form_response", $response->id, $form, $request );
// Return a success response.
return Response::send(
apply_filters(
- 'formgent_form_submission_response',
+ 'formgent_form_submission_response',
[ 'message' => esc_html__( 'The form was submitted successfully!', 'formgent' ) ],
$request, $form, $response
- ),
- 201
+ ),
+ 201
);
}
@@ -218,9 +227,9 @@
foreach ( $dtos as $dto ) {
/**
* @var AnswerDTO $dto
- *
+ *
* Prepare the AnswerDTO for storing in the database.
- *
+ *
* - Set the parent ID of the answer (from the previously retrieved parent field IDs).
* - Set the response ID to associate this answer with the current form response.
* - Convert the DTO to an array for insertion into the database.
@@ -250,11 +259,11 @@
$field_dtos = [];
$errors = [];
-
+
if ( ! is_array( $form_data ) ) {
return compact( 'field_dtos', 'errors' );
}
-
+
$children_request->set_body_params( $form_data );
$validator->wp_rest_request = $children_request;
$registered_fields = formgent_config( "fields" );
@@ -316,7 +325,7 @@
], 404
);
}
-
+
$dto = new ResponseDTO;
$dto->set_status( ResponseStatus::DRAFT )->set_is_completed( 0 )->set_form_id( $form_id );
--- a/formgent/app/Models/ResponseLog.php
+++ b/formgent/app/Models/ResponseLog.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace FormGentAppModels;
+
+defined( 'ABSPATH' ) || exit;
+
+use FormGentWpMVCApp;
+use FormGentWpMVCDatabaseResolver;
+use FormGentWpMVCDatabaseEloquentModel;
+
+class ResponseLog extends Model {
+ public static function get_table_name(): string {
+ return 'formgent_response_logs';
+ }
+
+ public function resolver(): Resolver {
+ return App::$container->get( Resolver::class );
+ }
+}
--- a/formgent/app/PaymentProcessors/Paypal.php
+++ b/formgent/app/PaymentProcessors/Paypal.php
@@ -18,7 +18,8 @@
use FormGentWpMVCRequestValidatorValidator;
use FormGentAppContractsPaymentInterface;
-class Paypal implements PaymentInterface {
+class Paypal implements PaymentInterface
+{
protected PaymentProcessor $payment_processor;
public function __construct() {
@@ -33,6 +34,12 @@
}
public function pay( PayDTO $pay_dto ) {
+ // Allow formgent-pro to handle subscription approval.
+ $result = apply_filters( 'formgent_paypal_pay', null, $pay_dto );
+ if ( $result !== null ) {
+ return $result;
+ }
+
$return_url_args = [
'order_id' => $pay_dto->order->get_id(),
'payment_id' => $pay_dto->payment->get_id(),
@@ -99,6 +106,12 @@
}
public function success( Validator $validator, WP_REST_Request $request ): PaymentReturnDTO {
+ // Allow formgent-pro to handle subscription callbacks (before legacy validation).
+ $result = apply_filters( 'formgent_paypal_success', null, $validator, $request );
+ if ( $result !== null ) {
+ return $result;
+ }
+
$validator->validate(
[
'token' => 'required|string',
--- a/formgent/app/PaymentProcessors/Stripe.php
+++ b/formgent/app/PaymentProcessors/Stripe.php
@@ -13,7 +13,8 @@
use FormGentStripeStripe as StripeSDK;
use WP_REST_Request;
-class Stripe implements PaymentInterface {
+class Stripe implements PaymentInterface
+{
public string $secret_key = '';
public function __construct() {
@@ -22,16 +23,22 @@
$this->secret_key = $payment_settings['stripe']['secret_key'];
}
-
+
public static function get_key(): string {
return 'stripe';
}
public function pay( PayDTO $pay_dto ) {
+ // Allow formgent-pro to handle subscription sessions.
+ $result = apply_filters( 'formgent_stripe_pay', null, $pay_dto );
+ if ( $result !== null ) {
+ return $result;
+ }
+
StripeSDK::setApiKey( $this->secret_key );
- $success_url = add_query_arg( [ 'session_id' => "{CHECKOUT_SESSION_ID}" ], get_rest_url( null, '/formgent/payment/success/stripe' ) );
- $cancel_url = add_query_arg( [ 'order_id' => $pay_dto->order->get_id(), 'payment_id' => $pay_dto->payment->get_id(), ], get_rest_url( null, '/formgent/payment/cancel/stripe' ) );
+ $success_url = add_query_arg( ['session_id' => "{CHECKOUT_SESSION_ID}"], get_rest_url( null, '/formgent/payment/success/stripe' ) );
+ $cancel_url = add_query_arg( ['order_id' => $pay_dto->order->get_id(), 'payment_id' => $pay_dto->payment->get_id(),], get_rest_url( null, '/formgent/payment/cancel/stripe' ) );
$line_items = [];
@@ -90,18 +97,26 @@
$session_id = $request->get_param( 'session_id' );
- $session = Session::retrieve( $session_id );
+ $session = Session::retrieve( $session_id, ['expand' => ['subscription', 'customer_details']] );
+
+ // Allow formgent-pro to handle subscription sessions.
+ $result = apply_filters( 'formgent_stripe_success', null, $session, $request );
+ if ( $result !== null ) {
+ return $result;
+ }
+
+ // One-time payment path (unchanged).
$payment_intent = PaymentIntent::retrieve( $session->payment_intent );
$transaction_id = $payment_intent->id;
$meta_data = $payment_intent->metadata;
$dto = ( new PaymentReturnDTO )
- ->set_order_id( $meta_data->order_id )
- ->set_payment_id( $meta_data->payment_id )
- ->set_transaction_id( $transaction_id )
- ->set_billing_email( $session->customer_details->email )
- ->set_billing_name( $session->customer_details->name )
- ->set_billing_country( $session->customer_details->address->country );
+ ->set_order_id( $meta_data->order_id )
+ ->set_payment_id( $meta_data->payment_id )
+ ->set_transaction_id( $transaction_id )
+ ->set_billing_email( $session->customer_details->email )
+ ->set_billing_name( $session->customer_details->name )
+ ->set_billing_country( $session->customer_details->address->country );
return $dto;
}
--- a/formgent/app/Providers/PaymentServiceProvider.php
+++ b/formgent/app/Providers/PaymentServiceProvider.php
@@ -14,7 +14,8 @@
use FormGentWpMVCExceptionsException;
use FormGentWpMVCViewView;
-class PaymentServiceProvider implements Provider {
+class PaymentServiceProvider implements Provider
+{
public function boot() {
add_filter( 'formgent_form_submission_response', [$this, 'maybe_add_payment_data'], 10, 4 );
add_shortcode( 'formgent_payment_success', [$this, 'payment_success_shortcode'] );
@@ -44,6 +45,42 @@
[$order_items, $order_total_amount] = $this->build_order_items( $fields, $form_data );
+ // Resolve the payment block field for subscription meta/validation.
+ $payment_field_attr = null;
+ foreach ( $fields as $field ) {
+ if ( ( $field['field_type'] ?? '' ) === 'payment' ) {
+ $payment_field_attr = $field;
+ break;
+ }
+ }
+ $is_subscription = ( ( $payment_field_attr['payment_type'] ?? '' ) === 'subscription' );
+
+ // Enforce allowed gateways for subscriptions (filterable by pro).
+ $allowed_gateways = apply_filters( 'formgent_subscription_allowed_gateways', ['stripe'], $payment_field_attr );
+ if ( $is_subscription && ! in_array( $payment_gateway, $allowed_gateways, true ) ) {
+ return array_merge(
+ $response_data,
+ [
+ 'payment_data' => [
+ 'success' => false,
+ 'message' => __( 'The selected payment method does not support subscriptions.', 'formgent' ),
+ 'code' => 422,
+ ],
+ ]
+ );
+ }
+
+ // Build subscription meta (populated by formgent-pro).
+ $subscription_meta = [];
+ if ( $is_subscription && $payment_field_attr ) {
+ $subscription_meta = apply_filters(
+ 'formgent_subscription_payment_meta',
+ [],
+ $payment_field_attr,
+ $form_data
+ );
+ }
+
$currency = formgent_settings_repository()->get_by_key( 'payment', [] )['currency'] ?? 'USD';
$order_dto = ( new OrderDTO )
@@ -67,7 +104,29 @@
$dto = ( new PayDTO )
->set_order( $order_dto )
->set_payment( $payment_dto )
- ->set_order_items( $order_item_dtos );
+ ->set_order_items( $order_item_dtos )
+ ->set_meta( $subscription_meta );
+
+ // Persist subscription meta for admin display and scheduled cancellations.
+ if ( $is_subscription && ! empty( $subscription_meta ) ) {
+ $meta_value = wp_json_encode(
+ array_merge(
+ $subscription_meta,
+ [
+ 'payment_id' => $payment_dto->get_id(),
+ 'order_id' => $order_dto->get_id(),
+ 'response_id' => $response->id,
+ 'gateway' => $payment_gateway,
+ 'currency' => $currency,
+ ]
+ )
+ );
+ formgent_response_repository()->update_meta(
+ $response->id,
+ 'formgent_subscription_meta_' . $payment_dto->get_id(),
+ $meta_value
+ );
+ }
try {
$response_data['payment_data'] = [
@@ -99,6 +158,21 @@
$items = [];
$total = 0;
+ // Check if we have a unified payment block
+ $has_unified_payment = false;
+ foreach ( $fields as $name => $field ) {
+ if ( $field['field_type'] === 'payment' ) {
+ $has_unified_payment = true;
+ break;
+ }
+ }
+
+ // If unified payment block exists, use it exclusively
+ if ( $has_unified_payment ) {
+ return $this->build_unified_order_items( $fields, $form_data );
+ }
+
+ // Legacy path: payment-item + custom-payment-amount + quantity
$quantity_fields = $this->get_quantity_field_map( $fields );
foreach ( $fields as $name => $field ) {
@@ -110,7 +184,8 @@
switch ( $field['field_type'] ) {
case 'payment-item':
- if ( ! isset( $form_data[$name] ) ) break;
+ if ( ! isset( $form_data[$name] ) )
+ break;
$display_type = $field['product_display_type'] ?? 'single';
@@ -148,8 +223,10 @@
case 'radio':
case 'dropdown':
+ case 'select':
$option = array_find(
- $field['options'], function ( $option ) use ( $form_data, $name ) {
+ $field['options'],
+ function ( $option ) use ( $form_data, $name ) {
return $option['value'] === $form_data[$name];
}
);
@@ -170,7 +247,8 @@
break;
case 'custom-payment-amount':
- if ( ! isset( $form_data[$name] ) ) break;
+ if ( ! isset( $form_data[$name] ) )
+ break;
$unit = abs( (float) ( $form_data[$name] ?? 0 ) );
$amount = $unit * $quantity;
@@ -186,6 +264,160 @@
}
}
+ return [$items, $total];
+ }
+
+ /**
+ * Build order items from the unified payment block configuration.
+ */
+ private function build_unified_order_items( array $fields, array $form_data ): array {
+ $items = [];
+ $total = 0;
+
+ $payment_field = null;
+ $payment_name = null;
+
+ foreach ( $fields as $name => $field ) {
+ if ( $field['field_type'] === 'payment' ) {
+ $payment_field = $field;
+ $payment_name = $name;
+ break;
+ }
+ }
+
+ if ( ! $payment_field ) {
+ return [$items, $total];
+ }
+
+ $payment_type = $payment_field['payment_type'] ?? 'one_time';
+ if ( $payment_type === 'subscription' ) {
+ // Processing is delegated to formgent-pro via filter.
+ return apply_filters( 'formgent_build_subscription_order_items', [$items, $total], $payment_field, $fields, $form_data );
+ }
+ if ( $payment_type !== 'one_time' ) {
+ return [$items, $total];
+ }
+
+ $amount_type = $payment_field['amount_type'] ?? 'fixed';
+ $quantity_enabled = ! empty( $payment_field['quantity_enabled'] );
+ $quantity_apply_to = $payment_field['quantity_apply_to'] ?? [];
+
+ if ( $amount_type === 'fixed' ) {
+ // Fixed amount uses a single payment-level quantity input
+ $quantity = 1;
+ if ( $quantity_enabled ) {
+ $qty_field_name = $payment_name . '_quantity';
+ $quantity = abs( (int) ( $form_data[$qty_field_name] ?? 1 ) );
+ if ( $quantity < 1 )
+ $quantity = 1;
+ }
+
+ $unit = abs( (float) ( $payment_field['fixed_price'] ?? 0 ) );
+ $amount = $unit * $quantity;
+ $items[] = [
+ 'title' => $payment_field['fixed_label'] ?? __( 'Payment', 'formgent' ),
+ 'unit_amount' => $unit,
+ 'quantity' => $quantity,
+ 'total_amount' => $amount,
+ ];
+ $total += $amount;
+ } elseif ( $amount_type === 'from_fields' ) {
+ // Amount from selected fields — each field has its own per-field quantity
+ $amount_field_names = $payment_field['amount_fields'] ?? [];
+
+ foreach ( $amount_field_names as $field_name ) {
+ if ( ! isset( $fields[$field_name], $form_data[$field_name] ) ) {
+ continue;
+ }
+
+ $ref_field = $fields[$field_name];
+ $ref_type = $ref_field['field_type'] ?? '';
+
+ // Read per-field quantity from form data
+ $apply_qty = $quantity_enabled && ! empty( $quantity_apply_to ) && in_array( $field_name, $quantity_apply_to, true );
+ if ( $apply_qty ) {
+ $per_field_qty_key = $field_name . '_quantity';
+ $field_qty = abs( (int) ( $form_data[$per_field_qty_key] ?? 1 ) );
+ if ( $field_qty < 1 ) $field_qty = 1;
+ } else {
+ $field_qty = 1;
+ }
+
+ switch ( $ref_type ) {
+ case 'number':
+ case 'custom-payment-amount':
+ $unit = abs( (float) ( $form_data[$field_name] ?? 0 ) );
+ $amount = $unit * $field_qty;
+ $items[] = [
+ 'title' => $ref_field['label'] ?? $field_name,
+ 'unit_amount' => $unit,
+ 'quantity' => $field_qty,
+ 'total_amount' => $amount,
+ ];
+ $total += $amount;
+ break;
+
+ case 'single-choice':
+ case 'dropdown':
+ // Single selection - match option price
+ if ( ! empty( $ref_field['options'] ) ) {
+ $selected_value = $form_data[$field_name];
+ // Could be an object like { value: true } or just a string
+ if ( is_array( $selected_value ) ) {
+ // Extract the actual selected value from the object
+ foreach ( $selected_value as $val => $checked ) {
+ if ( $checked ) {
+ $selected_value = $val;
+ break;
+ }
+ }
+ }
+ foreach ( $ref_field['options'] as $opt ) {
+ $raw_unit = $opt['price'] ?? ( $opt['numeric_value'] ?? ( $opt['numericValue'] ?? null ) );
+ if ( $opt['value'] === $selected_value && $raw_unit !== null && $raw_unit !== '' ) {
+ $unit = abs( (float) $raw_unit );
+ $amount = $unit * $field_qty;
+ $items[] = [
+ 'title' => $opt['label'] ?? $field_name,
+ 'unit_amount' => $unit,
+ 'quantity' => $field_qty,
+ 'total_amount' => $amount,
+ ];
+ $total += $amount;
+ break;
+ }
+ }
+ }
+ break;
+
+ case 'multiple-choice':
+ // Multi selection - sum selected options' prices
+ if ( ! empty( $ref_field['options'] ) && is_array( $form_data[$field_name] ) ) {
+ foreach ( $form_data[$field_name] as $val => $checked ) {
+ if ( ! $checked )
+ continue;
+ foreach ( $ref_field['options'] as $opt ) {
+ $raw_unit = $opt['price'] ?? ( $opt['numeric_value'] ?? ( $opt['numericValue'] ?? null ) );
+ if ( $opt['value'] === $val && $raw_unit !== null && $raw_unit !== '' ) {
+ $unit = abs( (float) $raw_unit );
+ $amount = $unit * $field_qty;
+ $items[] = [
+ 'title' => $opt['label'] ?? $val,
+ 'unit_amount' => $unit,
+ 'quantity' => $field_qty,
+ 'total_amount' => $amount,
+ ];
+ $total += $amount;
+ break;
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+
return [$items, $total];
}
--- a/formgent/app/Providers/PostTypeServiceProvider.php
+++ b/formgent/app/Providers/PostTypeServiceProvider.php
@@ -306,6 +306,45 @@
},
]
);
+
+ /**
+ * Form type meta (classic/general vs conversational)
+ */
+ register_post_meta(
+ 'formgent_form', '_formgent_type', [
+ 'type' => 'string',
+ 'single' => true,
+ 'default' => 'general',
+ 'show_in_rest' => [
+ 'schema' => [
+ 'type' => 'string',
+ ],
+ ],
+ 'auth_callback' => function() {
+ return current_user_can( 'edit_posts' );
+