Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
File Manager
/
wp-content
/
plugins
/
give
/
src
/
Subscriptions
/
Repositories
:
SubscriptionRepository.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?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'] ); } }