diff --git a/composer.json b/composer.json index 91c162e..aab78d4 100644 --- a/composer.json +++ b/composer.json @@ -2,13 +2,13 @@ "name": "vespolina/billing", "type": "library", "description": "Vespolina Billing.", - "keywords": ["vespolina", "billing", "ecommerce"], + "keywords": ["vespolina", "billing", "ecommerce", "recurring", "plans"], "homepage": "http://vespolina-project.org", "license": "MIT", "authors": [ { "name": "Vespolina Team", - "homepage": "https://github.com/vespolina/VespolinaPartner/contributors" + "homepage": "https://github.com/vespolina/VespolinaBilling" } ], "require": { diff --git a/lib/Vespolina/Billing/Handler/EntityHandlerInterface.php b/lib/Vespolina/Billing/Handler/EntityHandlerInterface.php index a102f2b..bdd7776 100755 --- a/lib/Vespolina/Billing/Handler/EntityHandlerInterface.php +++ b/lib/Vespolina/Billing/Handler/EntityHandlerInterface.php @@ -27,6 +27,17 @@ interface EntityHandlerInterface */ function createBillingAgreements($entity); + /** + * Init is called after a billing agreement has been created + * It copies relevant fields from the entity to the billing agreement + * + * @param $billingAgreement The billing agreement to be initialized + * @param $entity The main entity (eg. Order) + * @param $entityItem The item of the entity (eg. OrderItem) + * @return mixed + */ + function initBillingAgreement(BillingAgreementInterface $billingAgreement, $entity, $entityItem = null); + /** * Cancel the billing process for this entity * diff --git a/lib/Vespolina/Billing/Handler/OrderHandler.php b/lib/Vespolina/Billing/Handler/OrderHandler.php index f9cdb64..84de5f2 100755 --- a/lib/Vespolina/Billing/Handler/OrderHandler.php +++ b/lib/Vespolina/Billing/Handler/OrderHandler.php @@ -12,6 +12,7 @@ use Vespolina\Entity\Billing\BillingRequestInterface; use Vespolina\Billing\Handler\EntityHandlerInterface; use Vespolina\Entity\Order\OrderInterface; +use Vespolina\Entity\Order\ItemInterface; class OrderHandler implements EntityHandlerInterface { @@ -25,11 +26,36 @@ public function __construct(BillingManagerInterface $billingManager) public function createBillingAgreements($entity) { + $billingAgreements = array(); + $recurringItems = array(); //Initial set of detected recurring items + $recurringItemsMerged = array(); //Final set of recurring items after merge if (!$this->isBillable($entity)) { throw new \ErrorException('Entity is not billable'); } + //Collect items for which a recurring charge exists + /** @var Item $item **/ + foreach ($entity->getItems() as $item) { + $pricingSet = $item->getPricing(); + $pricingSet->getProcessed(); + + if ($pricingSet->get('recurringCharge')) { + + $recurringItems = $item; + } + } + + //Todo: merge items together + $recurringItems = $recurringItems; + + foreach ($recurringItems as $recurringItem) { + + // Check if we can attach this item to one of the existing billing agreements. + // If no suitable agreement can be found a new one is created + // If a suitable agreement is found the order item is attached to it + $this->createOrUpdateBusinessAgreements($billingAgreements, $recurringItem); + } } public function cancelBilling($entity) @@ -37,12 +63,72 @@ public function cancelBilling($entity) } + public function initBillingAgreement(BillingAgreementInterface $billingAgreement, $entity, $entityItem = null) + { + /** @var PartnerInterface $owner **/ + $owner = $entity->getOwner(); + + $paymentProfile = $owner->getPreferredPaymentProfile(); + + $billingAgreement + ->setPartner($owner) + ->setPaymentProfile($owner->getPaymentProfile()) + ; + } + public function isBillable($entity) { - //Currenlty we only check if the entity is valid but we should additional business logic here - // (eg. if the order is cancelled it should not eligable for billing) + if (!$this->isBillableEntity($entity)) return false; + + if (null == $entity->getOwner()) return false; + + return true; + } + + protected function createOrUpdateBusinessAgreements(array &$agreements, ItemInterface $item, $context = null) + { + $pricingSet = $item->getPricing(); + $interval = $pricingSet->get('interval'); + $cycles = $pricingSet->get('cycles'); + + if ($item->getAttribute('start_billing')) { + $startsOn = $item->getAttribute('start_billing'); + } elseif ($context['dueDate']) { + $startsOn = $pricingSet->get('startsOn'); + $date = explode(',', $startsOn->format('Y,m')); + $startsOn->setDate($date[0], $date[1], $context['dueDate']); + } else { + $startsOn = $pricingSet->get('startsOn'); + } + $startTimestamp = $startsOn->getTimestamp(); + + //Find a suitable billing agreement + $activeAgreement = null; + foreach ($agreements as $agreement) { + if ($agreement->getBillingInterval() == $interval && + $agreement->getBillingCycles() == $cycles && + $agreement->getInitialBillingDate()->getTimestamp() == $startTimestamp) { + $activeAgreement = $agreement; + } + } + + if (null == $activeAgreement) { + $activeAgreement = $this->billingManager->createBillingAgreement(); + $this->initBillingAgreement($item->getParent(), $item); + $activeAgreement + ->setInitialBillingDate($startsOn) + ->setNextBillingDate($startsOn) + ->setBillingCycles($pricingSet->get('cycles')) + ->setBillingInterval($pricingSet->get('interval')); + ; + $agreements[] = $activeAgreement; + } + + $activeAgreement->addOrderItem($item); + $activePricingSet = $activeAgreement->getPricing(); + $activeAgreement->setPricing($pricingSet->plus($activePricingSet)); - return $this->isBillableEntity($entity); + return $activeAgreement; } protected function isBillableEntity($entity) diff --git a/lib/Vespolina/Billing/Manager/BillingManager.php b/lib/Vespolina/Billing/Manager/BillingManager.php index 2533936..be1b245 100755 --- a/lib/Vespolina/Billing/Manager/BillingManager.php +++ b/lib/Vespolina/Billing/Manager/BillingManager.php @@ -91,6 +91,7 @@ public function findBillingAgreementById($id) */ public function billEntity($entity) { + $billingAgreements = array(); $entityHandler = $this->getEntityHandler($entity); if (null == $entityHandler) @@ -111,8 +112,7 @@ public function billEntity($entity) $this->gateway->updateBillingAgreement($billingAgreement); } - - return true; + return $billingAgreements; } /** @@ -129,21 +129,6 @@ public function createBillingAgreement() return new $this->billingAgreementClass(); } - /** - * @inheritdoc - */ - public function createBillingAgreements(OrderInterface $order) - { - $owner = $order->getOwner(); - $paymentProfile = $owner->getPreferredPaymentProfile(); - - $paymentProfileType = $paymentProfile->getType(); - $context = $this->context['billingAgreement'][$paymentProfileType]; - - $billingAgreements = $this->prepBillingAgreements($context, $owner, $paymentProfile, $order->getItems()); - - return $billingAgreements; - } /** * Rebuilds the billing agreement if there are any adjustments to the old agreement @@ -178,17 +163,10 @@ public function recreateBillingAgreementByPartnerAndPaymentProfile(Partner $part } /** - * @param $context - * @param \Vespolina\Entity\Partner\Partner $partner - * @param \Vespolina\Entity\Partner\PaymentProfile $paymentProfile - * @param $orderItems - * @return array - */ private function prepBillingAgreements($context, Partner $partner, PaymentProfile $paymentProfile, $orderItems) { $billingAgreements = array(); - /** @var Item $item **/ foreach ($orderItems as $item) { $pricingSet = $item->getPricing(); @@ -252,7 +230,7 @@ protected function addItemToAgreements(ItemInterface $item, array &$agreements, $this->gateway->persistBillingAgreement($activeAgreement); return $activeAgreement; - } + } */ public function processPendingBillingRequests() { diff --git a/lib/Vespolina/Billing/Manager/BillingManagerInterface.php b/lib/Vespolina/Billing/Manager/BillingManagerInterface.php index 0efdc2d..dbf8fd6 100755 --- a/lib/Vespolina/Billing/Manager/BillingManagerInterface.php +++ b/lib/Vespolina/Billing/Manager/BillingManagerInterface.php @@ -35,12 +35,6 @@ function billEntity($entity); */ function createBillingAgreement(); - /** - * @param \Vespolina\Entity\Order\OrderInterface $order - * @return array - */ - function createBillingAgreements(OrderInterface $order); - /** * Create a new billing request * diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..cac9d24 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,14 @@ + + + + + + ./tests + + + + + ./lib + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist deleted file mode 100644 index f6b55cf..0000000 --- a/phpunit.xml.dist +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - ./tests - - - - - - - diff --git a/tests/Manager/BillingManagerTest.php b/tests/Manager/BillingManagerTest.php index ad0c2bc..709f1cf 100644 --- a/tests/Manager/BillingManagerTest.php +++ b/tests/Manager/BillingManagerTest.php @@ -7,21 +7,16 @@ use Vespolina\Billing\Manager\BillingManager; use Vespolina\Billing\Gateway\BillingGateway; -use Vespolina\Entity\Order\OrderEvents; -use Vespolina\Entity\Order\Order; -use Vespolina\Entity\Partner\PaymentProfileType\Invoice; +use Vespolina\Entity\Billing\BillingAgreementInterface; use Vespolina\EventDispatcher\EventDispatcherInterface; use Vespolina\EventDispatcher\EventInterface; use Vespolina\Pricing\Entity\Element\RecurringElement; use Vespolina\Pricing\Entity\PricingSet; -use Vespolina\Billing\Tests\Manager\TestDispatcher; -use Vespolina\Billing\Tests\Manager\TestBaseManager; - /** * @group ecommerce */ -class BillingManagerTest extends TestBaseManager +class BillingManagerTest extends \PHPUnit_Framework_TestCase { protected $billingGateway; @@ -30,18 +25,13 @@ public function testConstruct() $this->markTestIncomplete('tests are need to make sure the contexts are set up correctly'); } - public function test() + public function testCreateBillingAgreement() { - $this->markTestIncomplete('the various pieces need to be tested with the events tha are triggered'); - // track states in processing - - /** current needs */ - // get order - // create current bill - // create recurring bill - // get CC - // submit current bill to CC - // create licenses when CC is approved + $billingManager = $this->createBillingManager(); + + $billingAgreement = $billingManager->createBillingAgreement(); + + $this->assertTrue($billingAgreement instanceof BillingAgreementInterface); } public function testCreateBillingAgreements() @@ -131,12 +121,9 @@ public function testGenerateRequestFromAgreement() protected function createTestOrder() { - $licenses = $this->getTestObjectsProvider()->loadLicenseProductDataFixtures(); - $upgrade = $this->getTestObjectsProvider()->loadUpgradeProductDataFixture(); - $user = $this->getTestObjectsProvider()->createRandomUser(); $order = $this->getOrderManager()->createOrder(); - $order->setPartner($user->getPartner()); + //$order->setOwner($user->getPartner()); $this->getOrderManager()->addProductToOrder($order, $licenses[License::PRODUCT_LICENSE_NAME], array(), 1, false); $this->getOrderManager()->addProductToOrder($order, $licenses[License::PRODUCT_LICENSE_NAME], array(), 1, false); @@ -152,18 +139,26 @@ protected function createTestOrder() protected function createBillingManager() { - $this->billingGateway = new BillingGateway($this->getMolino(), 'Vespolina\Entity\Billing\BillingAgreement'); + //$this->billingGateway = new BillingGateway($this->getMolino(), 'Vespolina\Entity\Billing\BillingAgreement'); + + $billingGateway = $this->getMockBuilder('Vespolina\Billing\Gateway\BillingGateway') + ->disableOriginalConstructor() + ->getMock() + ; + $eventDispatcher = new TestDispatcher(); $classMapping = array( 'billingAgreementClass' => 'Vespolina\Entity\Billing\BillingAgreement', 'billingRequestClass' => 'Vespolina\Entity\Billing\BillingRequest', ); - $contexts = array( - 'ImmersiveLabs\CaraCore\Context\InvoiceAgreementContext', - 'ImmersiveLabs\CaraCore\Context\InvoiceRequestContext', - ); - $manager = new BillingManager($this->billingGateway, $classMapping, $contexts, $eventDispatcher); + $contexts = array(); + $manager = new BillingManager($billingGateway, $classMapping, $contexts, $eventDispatcher); return $manager; } + + protected function getMolino() + { + + } } diff --git a/tests/Manager/TestBaseManager.php b/tests/Manager/TestBaseManager.php deleted file mode 100644 index d45d91b..0000000 --- a/tests/Manager/TestBaseManager.php +++ /dev/null @@ -1,246 +0,0 @@ - $env)); - static::$kernel->boot(); - $this->container = static::$kernel->getContainer(); - $this->em = $this->container->get('doctrine')->getManager(); - - $query = "UPDATE Vespolina\Entity\Partner\Partner p SET p.preferredPaymentProfile = NULL"; - $this->getEntityManager()->createQuery($query)->execute(); - - $this->purge(); - - $this->createCaraClient(); - - $this->initPermissions(); - $this->initProducts(); - } - - /** - * Check if there's queued email in the memory spool - * @param int $expectedContents - */ - protected function assertQueueContents($expectedContents = 1) - { - /** @var $transport \Swift_SpoolTransport */ - $transport = $this->getMailer()->getTransport(); - - $count = $transport->getSpool()->flushQueue($this->getNewMemoryTransport()); - - $this->assertEquals($expectedContents, $count); - } - - /** - * Create new memory transport so we can validate spool content - * @return \Swift_SpoolTransport - */ - protected function getNewMemoryTransport() - { - $newSpool = new \Swift_MemorySpool(); - - $newMemoryTransport = \Swift_SpoolTransport::newInstance($newSpool); - - return $newMemoryTransport; - } - - /** - * {@inheritDoc} - */ - protected function tearDown() - { - parent::tearDown(); - if($this->em) { - $this->em->close(); - } - } - - public function purge() - { - $purger = new ORMPurger($this->em); - $executor = new ORMExecutor($this->em, $purger); - - $query = "UPDATE Vespolina\Entity\Partner\Partner p SET p.preferredPaymentProfile = NULL"; - $this->getEntityManager()->createQuery($query)->execute(); - - $executor->purge(); - - /** - $projectEntities = array( - '\ImmersiveLabs\CaraCore\Entity\Tag', - '\ImmersiveLabs\CaraCore\Entity\License', - '\ImmersiveLabs\CaraCore\Entity\Impression', - '\ImmersiveLabs\OAuthServerBundle\Entity\Client', - '\ImmersiveLabs\CaraCore\Entity\User', - ); - - foreach ($projectEntities as $entityName) { - $this->getEntityManager() - ->createQuery(sprintf('DELETE FROM %s c', $entityName)) - ->execute() - ; - } - */ - } - - /** - * @return \Molino\Doctrine\ORM\Molino - */ - public function getMolino() - { - return $this->container->get('molino'); - } - - /** - * @return \Doctrine\ORM\EntityManager - */ - public function getEntityManager() - { - return $this->container->get('doctrine.orm.entity_manager'); - } - - /** - * Compare if two arrays have the same values - * - * @param $a1 - * @param $a2 - * @return boolean - */ - public function arraysAreEqual($a1, $a2) { - return !array_diff($a1, $a2) && !array_diff($a2, $a1); - } - - /** - * @return \Vespolina\Symfony2Bundle\EventDispatcher\EventDispatcher - */ - public function getEventDispatcher() - { - return $this->container->get('vespolina.event_dispatcher'); - } - - /** - * @return \Swift_Mailer - */ - public function getMailer() - { - return $this->container->get('mailer'); - } - - /** - * @return \Symfony\Component\HttpFoundation\Session\Session - */ - public function getSession() - { - return $this->container->get('session'); - } - - /** - * @return \JMS\Payment\CoreBundle\PluginController\EntityPluginController - */ - public function getPaymentPluginController() - { - return $this->container->get('payment.plugin_controller'); - } - - /** - * @return BillingInvoiceManagerInterface - */ - public function getBillingInvoiceManager() - { - return $this->container->get('vespolina.billing_invoice_manager'); - } - - /** - * @return OrderManagerInterface - */ - public function getOrderManager() - { - return $this->container->get('vespolina.order_manager'); - } - - /** - * @return ProductManagerInterface - */ - public function getProductManager() - { - return $this->container->get('vespolina.product_manager'); - } - -} - -class TestDispatcher implements EventDispatcherInterface -{ - protected $lastEvent; - protected $lastEventName; - - public function createEvent($subject = null) - { - $event = new Event($subject); - - return $event; - } - - public function dispatch($eventName, EventInterface $event = null) - { - $this->lastEvent = $event; - $this->lastEventName = $eventName; - } - - public function getLastEvent() - { - return $this->lastEvent; - } - - public function getLastEventName() - { - return $this->lastEventName; - } -} - -class Event implements EventInterface -{ - protected $name; - protected $subject; - - public function __construct($subject) - { - $this->subject = $subject; - } - - public function getName() - { - return $this->name; - } - - public function setName($name) - { - $this->name = $name; - } - - public function getSubject() - { - return $this->subject; - } -}