30, '49' => 60, '79' => 90, '99' => 120, '119' => 160, '149' => 200, '199' => 240, '249' => 340, '349' => 440, '499' => 590, '749' => 880, '1000' => 1200, '9999999' => 9999, ]; /** * The contact ID of this timebank. * @var int */ private $contactId; /** * The number of members. * @var int */ private $memberCount; /** * The number of billable members. It's $memberCount -2 with a minimum of zero. * @var int */ private $memberCountiBillable; /** * The price for one period of this timebank. * @var float */ private $price; /** * The creation date of the timebank. * @var DateTime */ private $creationDate; /** * The date the invoice is generated. * @var DateTime */ private $invoiceDate; /** * The date the invoice is due. * @var DateTime */ private $invoiceDueDate; /** * The date the invoice period begins. * @var DateTime */ private $periodBegin; /** * The date the invoice period begins. * @var DateTime */ private $periodEnd; /** * The biannual fee rate - same as the cost, but not pro-rated for new timebanks. * @var int */ private $biannualFeeRate; /** * An array of all the contact IDs for whom a contribution already exists in this billing period. * @var array */ public static $contributionExists = []; /** * A string that represents the current billing period - e.g. "2019-2". * First number is the year; second is 1 for Jan-Jun, 2 for Jul-Dec. * @var string */ public static $billingPeriod = NULL; /** * Class constructor. */ public function __construct($cid, $memberCount) { $this->cid = $cid; $this->memberCount = $memberCount; } /** * Generate invoices (called from API). * @param array $params An array containing all the values passed into the Invoicegen.generate API. * @return array APIv3 standard response. */ public static function generate($params) { $cid = $billingPeriod = NULL; if (isset($params['contact_id'])) { $cid = $params['contact_id']; } if (isset($params['billing_period'])) { $billingPeriod = $params['billing_period']; } self::setBillingPeriod($billingPeriod); // Get a list of contact IDs for everyone to generate an invoice for. $contacts = civicrm_api3('Contact', 'get', [ 'return' => ["id", self::BILLABLEMEMBERFIELD, self::TBCREATED, self::NUMBEROFMEMBERSOFFSET], 'contact_id' => $cid, 'contact_type' => 'Organization', self::TBBILLABLEFIELD => 1, 'options' => ['limit' => 0], ])['values']; self::setContributionExists(); foreach ($contacts as $k => $contact) { if (!in_array($k, self::$contributionExists)) { $tb = new CRM_Tbusainvoicegen_Timebank($k, $contact[self::BILLABLEMEMBERFIELD]); // Don't generate an invoice if there's no "TB Created" date. if (!$contact[self::TBCREATED]) { continue; } $tb->setMemberCountBillable($contact[self::NUMBEROFMEMBERSOFFSET]); $tb->creationDate = new DateTime($contact[self::TBCREATED]); $tb->setInvoiceDate($params); $tb->setDueDate($params); $tb->setPeriodBeginEndDate($params); $tb->setPrice(); $tb->createContribution(); } } } private function setMemberCountBillable($numberOfMembersOffset) { // Default offset is 2. $numberOfMembersOffset = $numberOfMembersOffset ?: 2; $this->memberCountBillable = $this->memberCount - $numberOfMembersOffset; if ($this->memberCountBillable < 0) { $this->memberCountBillable = 0; } } public static function setBillingPeriod($billingPeriod) { if ($billingPeriod) { self::$billingPeriod = $billingPeriod; } else { // Should we try to auto-calculate it here? Or nah? } } /** * Generate an array of contacts who already have a contribution created in this billing period. */ public static function setContributionExists() { $existing = []; $result = civicrm_api3('Contribution', 'get', [ self::BILLINGPERIODFIELD => self::$billingPeriod, 'options' => ['limit' => 0], ]); if ($result['count']) { foreach ($result['values'] as $contrib) { $existing[$contrib['contact_id']] = $contrib['contact_id']; } } self::$contributionExists = $existing; } private function setPrice() { if ($this->price) { return $this->price; } $adjustedMemberCount = $this->memberCountBillable; foreach ($this->priceArray as $members => $cost) { if ($adjustedMemberCount <= $members) { break; // After this "break", $cost will be accurate. } } $this->biannualFeeRate = $cost; // Pro-rate the payment if the timebank is new enough. $this->price = round($cost * $this->monthsProRated() / 6, 2); return $this->price; } /** * Sets the due date */ private function setInvoiceDate($params = []) { if (isset($params['invoice_date'])) { $this->invoiceDate = new DateTime($params['invoice_date']); } else { $this->invoiceDate = new DateTime(); } } /** * If the first annual anniversary of the TB is before the billing period, then the TB pays for the full billing period (6 months). * If the first annual anniversary of the TB is during any of the first 5 months of the billing period, then the TB pays a pro-rated fee for the number of full billing period months after the first annual anniversary of the TB (1 to 5 months). * If the first annual anniversary of the TB is during the 6th month of the current billing OR after the billing period ends, then the TB pays $0 for the billing period. */ private function monthsProRated() { // By default, we bill all 6 months in a billing period. $monthsProRated = 6; $beginDate = $this->periodBegin; $firstAnniversary = $this->creationDate->modify("+1 year"); $interval = $beginDate->diff($firstAnniversary); // First anniversary is before the billing period. if ($interval->invert == 1) { $monthsProRated = 6; } // Starts past the end of the billing period. elseif ($interval->y >= 1 || $interval->m >= 6) { $monthsProRated = 0; } // Falls within the billing period. else { $monthsProRated = 5 - $interval->m; } return $monthsProRated; } private function createContribution() { if (!$this->price) { return; } civicrm_api3('Contribution', 'create', [ 'financial_type_id' => 'CW License Fee', self::DUEDATE => $this->invoiceDueDate->format('Y-m-d'), self::INVOICEDATE => $this->invoiceDate->format('Y-m-d'), self::PERIODBEGIN => $this->periodBegin->format('Y-m-d'), self::PERIODEND => $this->periodEnd->format('Y-m-d'), self::BILLABLEMEMBERSCONTRIB => $this->memberCountBillable, self::BIANNUALFEERATE => $this->biannualFeeRate, 'total_amount' => $this->price, 'contact_id' => $this->cid, 'contribution_status_id' => 'Pending', 'is_pay_later' => 1, self::BILLINGPERIODFIELD => self::$billingPeriod, ]); } /** * Sets the due date */ private function setDueDate($params = []) { if (isset($params['due_date'])) { $this->invoiceDueDate = new DateTime($params['due_date']); } else { list($year, $number) = explode('-', self::$billingPeriod); if ($number == 1) { $this->invoiceDueDate = new DateTime($year . '-03-31'); } else { $this->invoiceDueDate = new DateTime($year . '-09-30'); } } } /** * Sets the period begin/end date. */ private function setPeriodBeginEndDate($params) { if (isset($params['period_begin_date'])) { $this->periodBegin = new DateTime($params['period_begin_date']); } else { list($year, $number) = explode('-', self::$billingPeriod); if ($number == 1) { $this->periodBegin = new DateTime($year . '-01-01'); } else { $this->periodBegin = new DateTime($year . '-07-01'); } } if (isset($params['period_end_date'])) { $this->periodEnd = new DateTime($params['period_end_date']); } else { list($year, $number) = explode('-', self::$billingPeriod); if ($number == 1) { $this->periodEnd = new DateTime($year . '-06-30'); } else { $this->periodEnd = new DateTime($year . '-12-31'); } } } /** * This is a helper function to generate values for the custom invoice template. * It takes a billing period (e.g. "2019-2") and returns an array $invoiceData. * $invoiceData[0] is the due date (e.g. "9/30/2019) * $invoiceData[1] is the billing period (e.g. "7/1/2019-12/31/2019"). * $invoiceData[2] is the total amount this contact owes. * @param int $contactId the contact ID of the timebank. * @param str $billingPeriod */ public static function invoiceData($contactId, $billingPeriod) { self::setBillingPeriod($billingPeriod); $invoiceData[2] = self::calculateTotalDue($contactId); return $invoiceData; } public static function calculateTotalDue($contactId) { // Get all contributions that aren't paid, sum up their total amounts. // Then get all those contributions' payments, sum up THEIR total amounts. // Subtract the second number from the first. $totalDue = 0; $contributions = civicrm_api3('Contribution', 'get', [ 'sequential' => 1, 'return' => ["total_amount"], 'contact_id' => $contactId, 'contribution_status_id' => ['!=' => "Completed"], 'options' => ['limit' => 0], ]); if ($contributions['count']) { foreach ($contributions['values'] as $contribution) { $totalDue += $contribution['total_amount']; $contributionIds[] = $contribution['id']; } } $payments = civicrm_api3('Payment', 'get', [ 'sequential' => 1, 'contribution_id' => ['IN' => $contributionIds], 'options' => ['limit' => 0], ]); if ($payments['count']) { foreach ($payments['values'] as $payment) { $totalDue -= $payment['total_amount']; } } return round($totalDue, 2); } }