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;
- }
-}