chore: add spec-kit and standardize signatures

- Added GitHub spec-kit for development workflow
- Standardized file signatures to Descomplicar® format
- Updated development configuration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Emanuel Almeida
2025-09-12 01:27:29 +01:00
parent 30ad448ed3
commit 4a7b232f68
50 changed files with 513565 additions and 0 deletions

View File

@@ -0,0 +1,657 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
/**
* Clinic Model
*
* Handles clinic entity operations and business logic
*
* @package KiviCare_API
* @subpackage Models
* @version 1.0.0
* @author Descomplicar® <dev@descomplicar.pt>
* @link https://descomplicar.pt
* @since 1.0.0
*/
namespace KiviCare_API\Models;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Clinic
*
* Model for handling clinic data and operations
*
* @since 1.0.0
*/
class Clinic {
/**
* Database table name
*
* @var string
*/
private static $table_name = 'kc_clinics';
/**
* Clinic ID
*
* @var int
*/
public $id;
/**
* Clinic properties
*
* @var array
*/
private $data = array();
/**
* Required fields for clinic creation
*
* @var array
*/
private static $required_fields = array(
'name',
'email',
'telephone_no',
'address',
'city',
'state',
'country',
'postal_code'
);
/**
* Constructor
*
* @param int|array $clinic_id_or_data Clinic ID or data array
* @since 1.0.0
*/
public function __construct( $clinic_id_or_data = null ) {
if ( is_numeric( $clinic_id_or_data ) ) {
$this->id = (int) $clinic_id_or_data;
$this->load_data();
} elseif ( is_array( $clinic_id_or_data ) ) {
$this->data = $clinic_id_or_data;
$this->id = isset( $this->data['id'] ) ? (int) $this->data['id'] : null;
}
}
/**
* Load clinic data from database
*
* @return bool True on success, false on failure
* @since 1.0.0
*/
private function load_data() {
if ( ! $this->id ) {
return false;
}
global $wpdb;
$table = $wpdb->prefix . self::$table_name;
$clinic_data = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d",
$this->id
),
ARRAY_A
);
if ( $clinic_data ) {
$this->data = $clinic_data;
return true;
}
return false;
}
/**
* Create a new clinic
*
* @param array $clinic_data Clinic data
* @return int|WP_Error Clinic ID on success, WP_Error on failure
* @since 1.0.0
*/
public static function create( $clinic_data ) {
// Validate required fields
$validation = self::validate_clinic_data( $clinic_data );
if ( is_wp_error( $validation ) ) {
return $validation;
}
global $wpdb;
$table = $wpdb->prefix . self::$table_name;
// Prepare data for insertion
$insert_data = array(
'name' => sanitize_text_field( $clinic_data['name'] ),
'email' => sanitize_email( $clinic_data['email'] ),
'telephone_no' => sanitize_text_field( $clinic_data['telephone_no'] ),
'specialties' => isset( $clinic_data['specialties'] ) ? wp_json_encode( $clinic_data['specialties'] ) : '',
'address' => sanitize_textarea_field( $clinic_data['address'] ),
'city' => sanitize_text_field( $clinic_data['city'] ),
'state' => sanitize_text_field( $clinic_data['state'] ),
'country' => sanitize_text_field( $clinic_data['country'] ),
'postal_code' => sanitize_text_field( $clinic_data['postal_code'] ),
'status' => isset( $clinic_data['status'] ) ? (int) $clinic_data['status'] : 1,
'clinic_admin_id' => isset( $clinic_data['clinic_admin_id'] ) ? (int) $clinic_data['clinic_admin_id'] : null,
'clinic_logo' => isset( $clinic_data['clinic_logo'] ) ? (int) $clinic_data['clinic_logo'] : null,
'profile_image' => isset( $clinic_data['profile_image'] ) ? (int) $clinic_data['profile_image'] : null,
'extra' => isset( $clinic_data['extra'] ) ? wp_json_encode( $clinic_data['extra'] ) : '',
'created_at' => current_time( 'mysql' )
);
$insert_data = array_map( array( self::class, 'prepare_for_db' ), $insert_data );
$result = $wpdb->insert( $table, $insert_data );
if ( $result === false ) {
return new \WP_Error(
'clinic_creation_failed',
'Failed to create clinic: ' . $wpdb->last_error,
array( 'status' => 500 )
);
}
return $wpdb->insert_id;
}
/**
* Update clinic data
*
* @param int $clinic_id Clinic ID
* @param array $clinic_data Updated clinic data
* @return bool|WP_Error True on success, WP_Error on failure
* @since 1.0.0
*/
public static function update( $clinic_id, $clinic_data ) {
if ( ! self::exists( $clinic_id ) ) {
return new \WP_Error(
'clinic_not_found',
'Clinic not found',
array( 'status' => 404 )
);
}
global $wpdb;
$table = $wpdb->prefix . self::$table_name;
// Prepare update data
$update_data = array();
$allowed_fields = array(
'name', 'email', 'telephone_no', 'specialties', 'address',
'city', 'state', 'country', 'postal_code', 'status',
'clinic_admin_id', 'clinic_logo', 'profile_image', 'extra'
);
foreach ( $allowed_fields as $field ) {
if ( isset( $clinic_data[ $field ] ) ) {
$value = $clinic_data[ $field ];
switch ( $field ) {
case 'email':
$update_data[ $field ] = sanitize_email( $value );
break;
case 'specialties':
case 'extra':
$update_data[ $field ] = is_array( $value ) ? wp_json_encode( $value ) : $value;
break;
case 'status':
case 'clinic_admin_id':
case 'clinic_logo':
case 'profile_image':
$update_data[ $field ] = (int) $value;
break;
case 'address':
$update_data[ $field ] = sanitize_textarea_field( $value );
break;
default:
$update_data[ $field ] = sanitize_text_field( $value );
}
}
}
if ( empty( $update_data ) ) {
return new \WP_Error(
'no_data_to_update',
'No valid data provided for update',
array( 'status' => 400 )
);
}
$update_data = array_map( array( self::class, 'prepare_for_db' ), $update_data );
$result = $wpdb->update(
$table,
$update_data,
array( 'id' => $clinic_id ),
null,
array( '%d' )
);
if ( $result === false ) {
return new \WP_Error(
'clinic_update_failed',
'Failed to update clinic: ' . $wpdb->last_error,
array( 'status' => 500 )
);
}
return true;
}
/**
* Delete a clinic
*
* @param int $clinic_id Clinic ID
* @return bool|WP_Error True on success, WP_Error on failure
* @since 1.0.0
*/
public static function delete( $clinic_id ) {
if ( ! self::exists( $clinic_id ) ) {
return new \WP_Error(
'clinic_not_found',
'Clinic not found',
array( 'status' => 404 )
);
}
// Check for dependencies
if ( self::has_dependencies( $clinic_id ) ) {
return new \WP_Error(
'clinic_has_dependencies',
'Cannot delete clinic with associated appointments or patients',
array( 'status' => 409 )
);
}
global $wpdb;
$table = $wpdb->prefix . self::$table_name;
$result = $wpdb->delete(
$table,
array( 'id' => $clinic_id ),
array( '%d' )
);
if ( $result === false ) {
return new \WP_Error(
'clinic_deletion_failed',
'Failed to delete clinic: ' . $wpdb->last_error,
array( 'status' => 500 )
);
}
return true;
}
/**
* Get clinic by ID
*
* @param int $clinic_id Clinic ID
* @return array|null Clinic data or null if not found
* @since 1.0.0
*/
public static function get_by_id( $clinic_id ) {
global $wpdb;
$table = $wpdb->prefix . self::$table_name;
$clinic = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d",
$clinic_id
),
ARRAY_A
);
if ( $clinic ) {
return self::format_clinic_data( $clinic );
}
return null;
}
/**
* Get all clinics with optional filtering
*
* @param array $args Query arguments
* @return array Array of clinic data
* @since 1.0.0
*/
public static function get_all( $args = array() ) {
global $wpdb;
$table = $wpdb->prefix . self::$table_name;
$defaults = array(
'status' => null,
'search' => '',
'limit' => 50,
'offset' => 0,
'orderby' => 'name',
'order' => 'ASC'
);
$args = wp_parse_args( $args, $defaults );
$where_clauses = array( '1=1' );
$where_values = array();
// Status filter
if ( ! is_null( $args['status'] ) ) {
$where_clauses[] = 'status = %d';
$where_values[] = $args['status'];
}
// Search filter
if ( ! empty( $args['search'] ) ) {
$where_clauses[] = '(name LIKE %s OR email LIKE %s OR city LIKE %s)';
$search_term = '%' . $wpdb->esc_like( $args['search'] ) . '%';
$where_values[] = $search_term;
$where_values[] = $search_term;
$where_values[] = $search_term;
}
$where_sql = implode( ' AND ', $where_clauses );
// Build query
$query = "SELECT * FROM {$table} WHERE {$where_sql}";
$query .= sprintf( ' ORDER BY %s %s',
sanitize_sql_orderby( $args['orderby'] ),
sanitize_sql_orderby( $args['order'] )
);
$query .= $wpdb->prepare( ' LIMIT %d OFFSET %d', $args['limit'], $args['offset'] );
if ( ! empty( $where_values ) ) {
$query = $wpdb->prepare( $query, $where_values );
}
$clinics = $wpdb->get_results( $query, ARRAY_A );
return array_map( array( self::class, 'format_clinic_data' ), $clinics );
}
/**
* Get total count of clinics
*
* @param array $args Filter arguments
* @return int Total count
* @since 1.0.0
*/
public static function get_count( $args = array() ) {
global $wpdb;
$table = $wpdb->prefix . self::$table_name;
$where_clauses = array( '1=1' );
$where_values = array();
if ( ! is_null( $args['status'] ?? null ) ) {
$where_clauses[] = 'status = %d';
$where_values[] = $args['status'];
}
if ( ! empty( $args['search'] ?? '' ) ) {
$where_clauses[] = '(name LIKE %s OR email LIKE %s OR city LIKE %s)';
$search_term = '%' . $wpdb->esc_like( $args['search'] ) . '%';
$where_values[] = $search_term;
$where_values[] = $search_term;
$where_values[] = $search_term;
}
$where_sql = implode( ' AND ', $where_clauses );
$query = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}";
if ( ! empty( $where_values ) ) {
$query = $wpdb->prepare( $query, $where_values );
}
return (int) $wpdb->get_var( $query );
}
/**
* Check if clinic exists
*
* @param int $clinic_id Clinic ID
* @return bool True if exists, false otherwise
* @since 1.0.0
*/
public static function exists( $clinic_id ) {
global $wpdb;
$table = $wpdb->prefix . self::$table_name;
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE id = %d",
$clinic_id
)
);
return (int) $count > 0;
}
/**
* Check if clinic has dependencies (appointments, patients, etc.)
*
* @param int $clinic_id Clinic ID
* @return bool True if has dependencies, false otherwise
* @since 1.0.0
*/
private static function has_dependencies( $clinic_id ) {
global $wpdb;
// Check appointments
$appointments_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments WHERE clinic_id = %d",
$clinic_id
)
);
if ( (int) $appointments_count > 0 ) {
return true;
}
// Check patient mappings
$patients_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_patient_clinic_mappings WHERE clinic_id = %d",
$clinic_id
)
);
if ( (int) $patients_count > 0 ) {
return true;
}
// Check doctor mappings
$doctors_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_doctor_clinic_mappings WHERE clinic_id = %d",
$clinic_id
)
);
return (int) $doctors_count > 0;
}
/**
* Validate clinic data
*
* @param array $clinic_data Clinic data to validate
* @return bool|WP_Error True if valid, WP_Error if invalid
* @since 1.0.0
*/
private static function validate_clinic_data( $clinic_data ) {
$errors = array();
// Check required fields
foreach ( self::$required_fields as $field ) {
if ( empty( $clinic_data[ $field ] ) ) {
$errors[] = "Field '{$field}' is required";
}
}
// Validate email format
if ( ! empty( $clinic_data['email'] ) && ! is_email( $clinic_data['email'] ) ) {
$errors[] = 'Invalid email format';
}
// Check for duplicate email
if ( ! empty( $clinic_data['email'] ) ) {
global $wpdb;
$table = $wpdb->prefix . self::$table_name;
$existing_clinic = $wpdb->get_var(
$wpdb->prepare(
"SELECT id FROM {$table} WHERE email = %s",
$clinic_data['email']
)
);
if ( $existing_clinic ) {
$errors[] = 'A clinic with this email already exists';
}
}
if ( ! empty( $errors ) ) {
return new \WP_Error(
'clinic_validation_failed',
'Clinic validation failed',
array(
'status' => 400,
'errors' => $errors
)
);
}
return true;
}
/**
* Format clinic data for API response
*
* @param array $clinic_data Raw clinic data
* @return array Formatted clinic data
* @since 1.0.0
*/
private static function format_clinic_data( $clinic_data ) {
if ( ! $clinic_data ) {
return null;
}
// Parse JSON fields
if ( ! empty( $clinic_data['specialties'] ) ) {
$clinic_data['specialties'] = json_decode( $clinic_data['specialties'], true ) ?: array();
}
if ( ! empty( $clinic_data['extra'] ) ) {
$clinic_data['extra'] = json_decode( $clinic_data['extra'], true ) ?: array();
}
// Cast numeric fields
$numeric_fields = array( 'id', 'status', 'clinic_admin_id', 'clinic_logo', 'profile_image' );
foreach ( $numeric_fields as $field ) {
if ( isset( $clinic_data[ $field ] ) ) {
$clinic_data[ $field ] = (int) $clinic_data[ $field ];
}
}
return $clinic_data;
}
/**
* Prepare data for database insertion/update
*
* @param mixed $value Data value
* @return mixed Prepared value
* @since 1.0.0
*/
private static function prepare_for_db( $value ) {
if ( is_null( $value ) ) {
return null;
}
if ( is_array( $value ) ) {
return wp_json_encode( $value );
}
return $value;
}
/**
* Get clinic statistics
*
* @param int $clinic_id Clinic ID
* @return array Clinic statistics
* @since 1.0.0
*/
public static function get_statistics( $clinic_id ) {
global $wpdb;
$stats = array(
'total_appointments' => 0,
'total_patients' => 0,
'total_doctors' => 0,
'revenue_this_month' => 0,
'appointments_today' => 0
);
// Total appointments
$stats['total_appointments'] = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments WHERE clinic_id = %d",
$clinic_id
)
);
// Total patients
$stats['total_patients'] = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_patient_clinic_mappings WHERE clinic_id = %d",
$clinic_id
)
);
// Total doctors
$stats['total_doctors'] = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_doctor_clinic_mappings WHERE clinic_id = %d",
$clinic_id
)
);
// Revenue this month
$stats['revenue_this_month'] = (float) $wpdb->get_var(
$wpdb->prepare(
"SELECT SUM(CAST(actual_amount AS DECIMAL(10,2)))
FROM {$wpdb->prefix}kc_bills
WHERE clinic_id = %d
AND MONTH(created_at) = MONTH(CURRENT_DATE())
AND YEAR(created_at) = YEAR(CURRENT_DATE())
AND payment_status = 'paid'",
$clinic_id
)
) ?: 0;
// Appointments today
$stats['appointments_today'] = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments
WHERE clinic_id = %d
AND appointment_start_date = CURDATE()",
$clinic_id
)
);
return $stats;
}
}