## $Id: payment.py 12839 2015-03-31 17:55:45Z henrik $
##
## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
##
"""
These are the payment tickets.
"""
import decimal
import grok
import uuid
from datetime import datetime
from zope.catalog.interfaces import ICatalog
from zope.component import getUtilitiesFor, getUtility
from zope.event import notify
from waeup.ikoba.interfaces import MessageFactory as _
from waeup.ikoba.utils.helpers import attrs_to_fields
from waeup.ikoba.payments.interfaces import (
    IPayment, STATE_UNPAID, STATE_FAILED, STATE_PAID,
    IPaymentGatewayService, IPayer, IPaymentItem, IPayee,
    IPaymentGatewayServicesLister, IPayableFinder, IPayerFinder,
    IPaymentWaitingForGatewayEvent, IPaymentFinishedEvent,
    )


def format_amount(amount, currency):
    """Turn `amount`, `currency` into a readable string.
    """
    cncy_map = {'USD': u'US$', 'EUR': u'\u20ac', 'NGN': u'\u20a6'}
    currency = cncy_map.get(currency, currency)
    return '%s %s' % (currency, '{:,.2f}'.format(amount))


def get_payment(payment_id):
    """Get payment by payment id.

    If no such payment can be found in catalog, return none.
    """
    cat = getUtility(ICatalog, name='payments_catalog')
    result_set = [x for x in cat.searchResults(
        payment_id=(payment_id, payment_id))]
    if len(result_set):
        return result_set[0]
    return None


def get_payments_from_payer_id(payer_id):
    """Get payments by payer id.

    If no such payment can be found in catalog, return none.
    """
    cat = getUtility(ICatalog, name='payments_catalog')
    result_set = [x for x in cat.searchResults(
        payer_id=(payer_id, payer_id))]
    if len(result_set):
        return result_set
    return None


def get_payments_from_payable_id(payable_id):
    """Get payments by payer id.

    If no such payment can be found in catalog, return none.
    """
    cat = getUtility(ICatalog, name='payments_catalog')
    result_set = [x for x in cat.searchResults(
        payable_id=(payable_id, payable_id))]
    if len(result_set):
        return result_set
    return None


def find_payable_from_payable_id(payable_id):
    """Find a payable from its id.

    Looks up all registered IPayableFinders and returns the first
    positive result found.
    """
    for name, util in getUtilitiesFor(IPayableFinder):
        result = util.get_payable_by_id(payable_id)
        if result is not None:
            return result
    return None


def find_payer_from_payer_id(payer_id):
    """Find a payer from its id.

    Looks up all registered IPayerFinders and returns the first
    positive result found.
    """
    for name, util in getUtilitiesFor(IPayerFinder):
        result = util.get_payer_by_id(payer_id)
        if result is not None:
            return result
    return None


def format_payment_item_values(payment_item_values, currency):
    """Format tuples (description, currency, amount) for output.

    `currency` passed in is the 'target' currency.

    Returns a list of formatted values. Last item is total sum.
    XXX: we do not really respect currency. If different items
    have different currencies, we are choked.
    """
    result = []
    total = decimal.Decimal("0.00")
    for descr, item_currency, amount in payment_item_values:
        total += amount
        if item_currency != currency:
            raise ValueError(
                "Different currencies in payment items not supported.")
        result.append((descr, '%s %0.2f' % (item_currency, amount)))
    result.append((_('Total'), '%s %0.2f' % (currency, total)))
    return result


def get_payment_providers():
    """Get all payment providers registered.
    """
    return dict(
        getUtilitiesFor(IPaymentGatewayService)
    )


class PaymentWaitingForGatewayEvent(object):
    grok.implements(IPaymentWaitingForGatewayEvent)

    def __init__(self, obj):
        self.object = obj


class PaymentFinishedEvent(object):
    grok.implements(IPaymentFinishedEvent)

    def __init__(self, obj):
        self.object = obj


class PaymentGatewayServicesLister(grok.GlobalUtility):
    grok.implements(IPaymentGatewayServicesLister)

    def __call__(self):
        """Get all services of payment gateways registered.
        """
        return get_payment_providers()


class PaymentProviderServiceBase(grok.GlobalUtility):
    """Base for IPaymentGatewayServices.
    """
    grok.baseclass()
    grok.implements(IPaymentGatewayService)

    title = u'Sample Credit Card Service'

    def store(self, payment):
        """Store `payment` in site.
        """
        site = grok.getSite()
        payments = site['payments']
        if payment.payment_id in payments:
            del site['payments'][payment.payment_id]
        site['payments'][payment.payment_id] = payment


class Payment(grok.Model):
    """This is a payment.
    """
    grok.implements(IPayment)
    grok.provides(IPayment)

    form_fields_interface = IPayment

    @property
    def amount(self):
        """Total amount.

        Sum of all contained payment items.
        """
        amounts = [item.amount for item in self.payment_items]
        return sum(amounts, decimal.Decimal("0.00"))

    def __init__(self, payer, payable, payee=None):
        super(Payment, self).__init__()
        items = tuple([x for x in payable.payment_items])
        self.payment_items = items
        self.payer_id = payer.payer_id
        self.payable_id = payable.payable_id
        self.title = payable.title
        self.creation_date = datetime.utcnow()
        self.payment_date = None
        self.payment_id = unicode(uuid.uuid4().hex)
        self.state = STATE_UNPAID
        self.currency = payable.currency
        if payee is not None:
            self.payee_id = payee.payee_id
        return

    def approve(self, payment_date=None):
        """A payment was approved.

        Successful ending; the payment is marked as payed.

        If `payment_date` is given, it must be a datetime object
        giving a datetime in UTC timezone.

        Raises ObjectModifiedEvent.
        """
        if payment_date is None:
            payment_date = datetime.utcnow()
        self.payment_date = payment_date
        self.state = STATE_PAID
        notify(grok.ObjectModifiedEvent(self))

    def mark_failed(self, reason=None):
        """Mark payment as failed.

        Raises ObjectModifiedEvent.
        """
        self.state = STATE_FAILED
        notify(grok.ObjectModifiedEvent(self))

Payment = attrs_to_fields(Payment, omit="amount")


@attrs_to_fields
class Payer(object):
    """A Payer for testing.

    It cannot be stored in ZODB.
    """
    grok.implements(IPayer)


@attrs_to_fields
class PaymentItem(object):

    grok.implements(IPaymentItem)

    def __init__(
            self, title=u"", amount=decimal.Decimal("0.00")):
        super(PaymentItem, self).__init__()
        self.title = title
        self.amount = amount

    def __repr__(self):
        result = "%s(title=%r, amount=%r)" % (
            self.__class__.__name__, self.title, self.amount)
        return result

    def to_string(self):
        """A string representation that can be used in exports.

        Returned is a unicode string of format
        ``(u'<ITEM_ID>', u'<TITLE>', u'<AMOUNT>')``.
        """
        string = u"(u'%s', u'%s')" % (self.title, self.amount)
        string = string.replace("u'None'", "None")
        return string


@attrs_to_fields
class Payee(object):
    """Someone being paid.

    This is for testing only and cannot be stored in ZODB.
    """
    grok.implements(IPayee)
