• File: SubscriptionRepository.php
  • Full Path: /home/bravrvjk/itiministry.org/wp-content/plugins/give/src/Subscriptions/Repositories/SubscriptionRepository.php
  • Date Modified: 10/30/2025 12:17 AM
  • File size: 18.98 KB
  • MIME-type: text/x-php
  • Charset: utf-8
<?php

namespace Give\Subscriptions\Repositories;

use Exception;
use Give\Donations\Models\Donation;
use Give\Donations\ValueObjects\DonationMetaKeys;
use Give\Donations\ValueObjects\DonationMode;
use Give\Donations\ValueObjects\DonationStatus;
use Give\Donations\ValueObjects\DonationType;
use Give\Framework\Database\DB;
use Give\Framework\Exceptions\Primitives\InvalidArgumentException;
use Give\Framework\Models\ModelQueryBuilder;
use Give\Framework\Support\Facades\DateTime\Temporal;
use Give\Helpers\Hooks;
use Give\Log\Log;
use Give\Subscriptions\Models\Subscription;
use Give\Subscriptions\ValueObjects\SubscriptionMode;
use Give\Subscriptions\ValueObjects\SubscriptionStatus;

/**
 * @since 4.8.0 Add notes repository
 * @since 2.19.6
 */
class SubscriptionRepository
{
    /**
     * @var SubscriptionNotesRepository
     */
    public $notes;

    /**
     * @var string[]
     */
    private $requiredSubscriptionProperties = [
        'donorId',
        'period',
        'frequency',
        'amount',
        'status',
        'donationFormId',
    ];

    /**
     * @since 4.8.0
     */
    public function __construct()
    {
        $this->notes = give(SubscriptionNotesRepository::class);
    }

    /**
     * @since 2.19.6
     *
     * @return Subscription|null
     */
    public function getById(int $subscriptionId)
    {
        return $this->queryById($subscriptionId)->get();
    }

    /**
     * @since 2.27.0  Add support for multiple return types.
     * @since 2.21.0
     *
     * @return Subscription|null
     */
    public function getByGatewaySubscriptionId(string $gatewaySubscriptionId)
    {
        return $this->queryByGatewaySubscriptionId($gatewaySubscriptionId)->get();
    }

    /**
     * @since 2.19.6
     *
     * @param int $subscriptionId
     *
     * @return ModelQueryBuilder<Subscription>
     */
    public function queryById(int $subscriptionId): ModelQueryBuilder
    {
        return $this->prepareQuery()
            ->where('id', $subscriptionId);
    }

    /**
     * @since 2.21.0
     *
     * @return ModelQueryBuilder<Subscription>
     */
    public function queryByGatewaySubscriptionId(string $gatewaySubscriptionId): ModelQueryBuilder
    {
        return $this->prepareQuery()
            ->where('profile_id', $gatewaySubscriptionId);
    }

    /**
     * @since 2.19.6
     *
     * @param int $donationId
     *
     * @return ModelQueryBuilder<Subscription>
     */
    public function queryByDonationId(int $donationId): ModelQueryBuilder
    {
        return $this->prepareQuery()
            ->where('parent_payment_id', $donationId);
    }

    /**
     * @since 2.19.6
     *
     * @param int $donorId
     *
     * @return ModelQueryBuilder<Subscription>
     */
    public function queryByDonorId(int $donorId): ModelQueryBuilder
    {
        return $this->prepareQuery()
            ->where('customer_id', $donorId);
    }

    /**
     * @since 4.11.0
     *
     * @param int $campaignId
     *
     * @return ModelQueryBuilder<Subscription>
     */
    public function queryByCampaignId(int $campaignId): ModelQueryBuilder
    {
        return $this->prepareQuery()
            ->where('campaign_id', $campaignId);
    }

    /**
     * @deprecated Use give()->subscriptions->notes()->queryBySubscriptionId()->getAll() instead.
     * @since 2.19.6
     *
     * @return object[]
     */
    public function getNotesBySubscriptionId(int $id): array
    {
        _give_deprecated_function(__METHOD__, '4.6.0', 'give()->subscriptions->notes()->queryBySubscriptionId()->getAll()');

        $notes = array_map(
            static function ($note) {
                return array_merge($note->toArray(), [
                    'note' => $note->comment_content,
                    'date' => $note->comment_date,
                ]);
            },
            $this->notes->queryBySubscriptionId($id)->getAll()
        );

        if (!$notes) {
            return [];
        }

        return $notes;
    }

    /**
     * @since 4.11.0 add campaign_id column to insert
     * @since 2.24.0 add payment_mode column to insert
     * @since 2.21.0 replace actions with givewp_subscription_creating and givewp_subscription_created
     * @since 2.19.6
     *
     * @return void
     * @throws Exception
     */
    public function insert(Subscription $subscription)
    {
        $this->validateSubscription($subscription);

        if ($subscription->renewsAt === null) {
            $subscription->bumpRenewalDate();
        }

        // If the subscription is not associated with a campaign, try to find the campaign ID by the form ID
        if (!$subscription->campaignId) {
            $campaign = give()->campaigns->getByFormId($subscription->donationFormId);
            $subscription->campaignId = $campaign ? $campaign->id : null;
        }

        Hooks::doAction('givewp_subscription_creating', $subscription);

        $dateCreated = Temporal::withoutMicroseconds($subscription->createdAt ?: Temporal::getCurrentDateTime());

        DB::query('START TRANSACTION');

        try {
            DB::table('give_subscriptions')->insert([
                'created' => Temporal::getFormattedDateTime($dateCreated),
                'expiration' => Temporal::getFormattedDateTime($subscription->renewsAt),
                'status' => $subscription->status->getValue(),
                'profile_id' => $subscription->gatewaySubscriptionId ?? '',
                'customer_id' => $subscription->donorId,
                'period' => $subscription->period->getValue(),
                'frequency' => $subscription->frequency,
                'initial_amount' => $subscription->amount->formatToDecimal(),
                'recurring_amount' => $subscription->amount->formatToDecimal(),
                'recurring_fee_amount' => $subscription->feeAmountRecovered !== null ? $subscription->feeAmountRecovered->formatToDecimal(
                ) : 0,
                'bill_times' => $subscription->installments,
                'transaction_id' => $subscription->transactionId ?? '',
                'product_id' => $subscription->donationFormId,
                'campaign_id' => $subscription->campaignId,
                'payment_mode' => $subscription->mode->getValue(),
            ]);
        } catch (Exception $exception) {
            DB::query('ROLLBACK');

            Log::error('Failed creating a subscription', compact('subscription'));

            throw new $exception('Failed creating a subscription');
        }

        DB::query('COMMIT');

        $subscriptionId = DB::last_insert_id();

        $subscription->id = $subscriptionId;
        $subscription->createdAt = $dateCreated;

        Hooks::doAction('givewp_subscription_created', $subscription);
    }

    /**
     * @since 4.11.0 add campaign_id column to update
     * @since 3.17.0 add expiration column to update
     * @since 2.24.0 add payment_mode column to update
     * @since 2.21.0 replace actions with givewp_subscription_updating and givewp_subscription_updated
     * @since 2.19.6
     *
     * @return void
     * @throws Exception
     */
    public function update(Subscription $subscription)
    {
        $this->validateSubscription($subscription);

        if ($subscription->renewsAt === null) {
            throw new InvalidArgumentException('renewsAt is required.');
        }

        Hooks::doAction('givewp_subscription_updating', $subscription);

        DB::query('START TRANSACTION');

        try {
            DB::table('give_subscriptions')
                ->where('id', $subscription->id)
                ->update([
                    'expiration' => Temporal::getFormattedDateTime($subscription->renewsAt),
                    'status' => $subscription->status->getValue(),
                    'profile_id' => $subscription->gatewaySubscriptionId,
                    'customer_id' => $subscription->donorId,
                    'period' => $subscription->period->getValue(),
                    'frequency' => $subscription->frequency,
                    'initial_amount' => $subscription->amount->formatToDecimal(),
                    'recurring_amount' => $subscription->amount->formatToDecimal(),
                    'recurring_fee_amount' => isset($subscription->feeAmountRecovered) ? $subscription->feeAmountRecovered->formatToDecimal(
                    ) : 0,
                    'bill_times' => $subscription->installments,
                    'transaction_id' => $subscription->transactionId ?? '',
                    'product_id' => $subscription->donationFormId,
                    'campaign_id' => $subscription->campaignId,
                    'payment_mode' => $subscription->mode->getValue(),
                ]);
        } catch (Exception $exception) {
            DB::query('ROLLBACK');

            Log::error('Failed updating a subscription', compact('subscription'));

            throw new $exception('Failed updating a subscription');
        }

        DB::query('COMMIT');

        Hooks::doAction('givewp_subscription_updated', $subscription);
    }

    /**
     * @since 2.21.0 replace actions with givewp_subscription_deleting and givewp_subscription_deleted
     * @since 2.20.0 consolidate meta deletion into a single query
     * @since 2.19.6
     *
     * @throws Exception
     */
    public function delete(Subscription $subscription): bool
    {
        Hooks::doAction('givewp_subscription_deleting', $subscription);

        DB::query('START TRANSACTION');

        try {
            DB::table('give_subscriptions')
                ->where('id', $subscription->id)
                ->delete();

            DB::table('give_subscriptionmeta')
                ->where('subscription_id', $subscription->id)
                ->delete();
        } catch (Exception $exception) {
            DB::query('ROLLBACK');

            Log::error('Failed deleting a subscription', compact('subscription'));

            throw new $exception('Failed deleting a subscription');
        }

        DB::query('COMMIT');

        Hooks::doAction('givewp_subscription_deleted', $subscription);

        return true;
    }

    /**
     * @since 4.8.0
     *
     * @throws Exception
     */
    public function trash(Subscription $subscription): bool
    {
        DB::query('START TRANSACTION');

        Hooks::doAction('givewp_subscription_trashing', $subscription);

        try {
            $previousStatus = DB::table('give_subscriptions')
                ->where('id', $subscription->id)
                ->value('status');

            give()->subscription_meta->update_meta($subscription->id, '_wp_trash_meta_status', $previousStatus);
            give()->subscription_meta->update_meta($subscription->id, '_wp_trash_meta_time', time());

            DB::table('give_subscriptions')
                ->where('id', $subscription->id)
                ->update(['status' => SubscriptionStatus::TRASHED()->getValue()]);
        } catch (Exception $exception) {
            DB::query('ROLLBACK');

            Log::error('Failed trashing a subscription', compact('subscription'));

            throw new $exception('Failed trashing a subscription');
        }

        DB::query('COMMIT');

        Hooks::doAction('givewp_subscription_trashed', $subscription);

        return true;
    }

    /**
     * @since 4.12.0
     *
     * @throws Exception
     */
    public function unTrash(Subscription $subscription): bool
    {
        DB::query('START TRANSACTION');

        Hooks::doAction('givewp_subscription_untrashing', $subscription);

        try {
            $previousStatus = give()->subscription_meta->get_meta($subscription->id, '_wp_trash_meta_status', true);

            // If no previous status was saved, default to 'active'
            if (empty($previousStatus)) {
                $previousStatus = SubscriptionStatus::ACTIVE;
            }

            DB::table('give_subscriptions')
                ->where('id', $subscription->id)
                ->update(['status' => $previousStatus]);
        } catch (Exception $exception) {
            DB::query('ROLLBACK');

            Log::error('Failed untrashing a subscription', compact('subscription'));

            throw new $exception('Failed untrashing a subscription');
        }

        DB::query('COMMIT');

        Hooks::doAction('givewp_subscription_untrashed', $subscription);

        return true;
    }

    /**
     * Up to this point the donation is created first and then the subscription, and the donation is stored as the
     * parent_payment_id of the subscription. This is backwards and should not be the case. But legacy code depends on
     * this value, so we still need to store it for now.
     *
     * This should only be used when creating a new Subscription with its corresponding Donation. Do not add this value
     * to the Subscription model as it should not be reference moving forward.
     *
     * @since 2.24.0 Save payment mode to subscription meta
     * @since 2.23.0
     *
     * @return void
     */
    public function updateLegacyParentPaymentId(int $subscriptionId, int $donationId)
    {
        $mode = give_get_meta($donationId, DonationMetaKeys::MODE, true) ?? (give_is_test_mode() ? 'test' : 'live');

        DB::table('give_subscriptions')
            ->where('id', $subscriptionId)
            ->update([
                'parent_payment_id' => $donationId,
                'payment_mode' => $mode,
            ]);
    }

    /**
     * @since 2.19.6
     *
     * @throws Exception
     */
    public function updateLegacyColumns(int $subscriptionId, array $columns): bool
    {
        foreach (Subscription::propertyKeys() as $key) {
            if (array_key_exists($key, $columns)) {
                throw new InvalidArgumentException("'$key' is not a legacy column.");
            }
        }

        DB::query('START TRANSACTION');

        try {
            DB::table('give_subscriptions')
                ->where('id', $subscriptionId)
                ->update($columns);
        } catch (Exception $exception) {
            DB::query('ROLLBACK');

            Log::error('Failed updating a subscription', compact('subscriptionId', 'columns'));

            throw new $exception('Failed updating a subscription');
        }

        DB::query('COMMIT');

        return true;
    }

    /**
     * Sets the payment mode for a given subscription
     *
     * @since 2.24.0
     *
     * @return void
     */
    public function updatePaymentMode(int $subscriptionId, SubscriptionMode $mode)
    {
        DB::table('give_subscriptions')
            ->where('id', $subscriptionId)
            ->update([
                'payment_mode' => $mode->getValue(),
            ]);
    }

    /**
     * @since 2.23.0 update to no longer rely on parent_payment_id column as it will be deprecated
     * @since 2.19.6
     *
     * @return int|null
     */
    public function getInitialDonationId(int $subscriptionId)
    {
        $query = DB::table('posts')
            ->select('ID')
            ->attachMeta(
                'give_donationmeta',
                'ID',
                'donation_id',
                [DonationMetaKeys::SUBSCRIPTION_ID, 'subscriptionId'],
                [DonationMetaKeys::SUBSCRIPTION_INITIAL_DONATION, 'initialDonationId']
            )
            ->where('give_donationmeta_attach_meta_subscriptionId.meta_value', $subscriptionId)
            ->where('give_donationmeta_attach_meta_initialDonationId.meta_value', 1)
            ->get();

        if (!$query) {
            return null;
        }

        return (int)$query->ID;
    }

    /**
     * @since 4.11.0 add campaignId to renewal
     * @since 4.8.1 Remove campaignId from the attributes array since it is auto-generated based on the subscription's form.
     * @since 4.8.0 Add campaignId support.
     * @since 3.20.0
     * @throws Exception
     */
   public function createRenewal(Subscription $subscription, array $attributes = []): Donation
   {
       $initialDonation = $subscription->initialDonation();

        $donation = Donation::create(
            array_merge([
                'subscriptionId' => $subscription->id,
                'gatewayId' => $subscription->gatewayId,
                'amount' => $subscription->amount,
                'status' => DonationStatus::COMPLETE(),
                'type' => DonationType::RENEWAL(),
                'donorId' => $subscription->donorId,
                'formId' => $subscription->donationFormId,
                'honorific' => $initialDonation->honorific,
                'firstName' => $initialDonation->firstName,
                'lastName' => $initialDonation->lastName,
                'email' => $initialDonation->email,
                'phone' => $initialDonation->phone,
                'anonymous' => $initialDonation->anonymous,
                'levelId' => $initialDonation->levelId,
                'company' => $initialDonation->company,
                'comment' => $initialDonation->comment,
                'billingAddress' => $initialDonation->billingAddress,
                'feeAmountRecovered' => $subscription->feeAmountRecovered,
                'exchangeRate' => $initialDonation->exchangeRate,
                'formTitle' => $initialDonation->formTitle,
                'mode' => $subscription->mode->isLive() ? DonationMode::LIVE() : DonationMode::TEST(),
                'donorIp' => $initialDonation->donorIp,
                'campaignId' => $subscription->campaignId,
            ], $attributes)
        );

        $subscription->bumpRenewalDate();
        $subscription->save();

        return $donation;
    }

    /**
     * @since 2.19.6
     *
     * @return void
     */
    private function validateSubscription(Subscription $subscription)
    {
        foreach ($this->requiredSubscriptionProperties as $key) {
            if (!isset($subscription->$key)) {
                throw new InvalidArgumentException("'$key' is required.");
            }
        }

        if (!$subscription->donor) {
            throw new InvalidArgumentException("Invalid donorId, Donor does not exist");
        }
    }

    /**
     * @since 4.11.0 add campaign_id column to select
     * @since 2.19.6
     *
     * @return ModelQueryBuilder<Subscription>
     */
    public function prepareQuery(): ModelQueryBuilder
    {
        $builder = new ModelQueryBuilder(Subscription::class);

        return $builder->from('give_subscriptions')
            ->select(
                'id',
                ['created', 'createdAt'],
                ['expiration', 'renewsAt'],
                ['customer_id', 'donorId'],
                'period',
                ['frequency', 'frequency'],
                ['bill_times', 'installments'],
                ['transaction_id', 'transactionId'],
                ['payment_mode', 'mode'],
                ['recurring_amount', 'amount'],
                ['recurring_fee_amount', 'feeAmount'],
                'status',
                ['profile_id', 'gatewaySubscriptionId'],
                ['product_id', 'donationFormId'],
                ['campaign_id', 'campaignId']
            )
            ->attachMeta(
                'give_donationmeta',
                'parent_payment_id',
                'donation_id',
                [DonationMetaKeys::GATEWAY, 'gatewayId'],
                [DonationMetaKeys::CURRENCY, 'currency']
            );
    }
}