182 lines
5.1 KiB
Perl
182 lines
5.1 KiB
Perl
|
|
#!/usr/bin/perl
|
||
|
|
#
|
||
|
|
# DW::Shop::Engine::Stripe
|
||
|
|
#
|
||
|
|
# Interfaces to Stripe for processing payments.
|
||
|
|
#
|
||
|
|
# Authors:
|
||
|
|
# Mark Smith <mark@dreamwidth.org>
|
||
|
|
#
|
||
|
|
# Copyright (c) 2019 by Dreamwidth Studios, LLC.
|
||
|
|
#
|
||
|
|
# This program is free software; you may redistribute it and/or modify it under
|
||
|
|
# the same terms as Perl itself. For a copy of the license, please reference
|
||
|
|
# 'perldoc perlartistic' or 'perldoc perlgpl'.
|
||
|
|
#
|
||
|
|
|
||
|
|
package DW::Shop::Engine::Stripe;
|
||
|
|
|
||
|
|
use strict;
|
||
|
|
use Carp qw/ croak confess /;
|
||
|
|
use HTTP::Request::Common;
|
||
|
|
use LWP::UserAgent;
|
||
|
|
use URI::Escape;
|
||
|
|
|
||
|
|
use DW::Shop::Cart;
|
||
|
|
use LJ::JSON;
|
||
|
|
|
||
|
|
use base qw/ DW::Shop::Engine /;
|
||
|
|
|
||
|
|
# new( $cart )
|
||
|
|
#
|
||
|
|
# instantiates a new engine for the given cart
|
||
|
|
sub new {
|
||
|
|
return bless { cart => $_[1] }, $_[0];
|
||
|
|
}
|
||
|
|
|
||
|
|
# _encode()
|
||
|
|
#
|
||
|
|
# encode for stripe's url form encoded API. why the heck can't I find a module that does
|
||
|
|
# this for me!?
|
||
|
|
sub _encode {
|
||
|
|
my $data = $_[0];
|
||
|
|
|
||
|
|
my $encode = sub {
|
||
|
|
my ( $key, $val ) = @_;
|
||
|
|
my @rvs;
|
||
|
|
|
||
|
|
if ( ref $val eq 'ARRAY' ) {
|
||
|
|
my $ct = 0;
|
||
|
|
foreach my $item (@$val) {
|
||
|
|
if ( ref $item eq 'HASH' ) {
|
||
|
|
foreach my $subkey ( keys %$item ) {
|
||
|
|
push @rvs,
|
||
|
|
uri_escape(qq{$key\[$ct][$subkey]}) . '='
|
||
|
|
. uri_escape( $item->{$subkey} );
|
||
|
|
}
|
||
|
|
}
|
||
|
|
elsif ( ref $item ) {
|
||
|
|
confess 'expected hashref or scalar';
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
push @rvs, uri_escape(qq{$key\[$ct]}) . '=' . uri_escape($item);
|
||
|
|
}
|
||
|
|
$ct += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
push @rvs, uri_escape($key) . '=' . uri_escape($val);
|
||
|
|
}
|
||
|
|
return join '&', @rvs;
|
||
|
|
};
|
||
|
|
|
||
|
|
return join '&', map { $encode->( $_, $data->{$_}, '' ) } keys %$data;
|
||
|
|
}
|
||
|
|
|
||
|
|
# _post()
|
||
|
|
sub _post {
|
||
|
|
my ( $path, $data ) = @_;
|
||
|
|
|
||
|
|
my $ua = LWP::UserAgent->new;
|
||
|
|
$ua->agent('Dreamwidth Payment API <accounts@dreamwidth.org>');
|
||
|
|
|
||
|
|
return $ua->post(
|
||
|
|
qq{https://api.stripe.com/v1/$path},
|
||
|
|
Content => _encode($data),
|
||
|
|
Authorization => qq|Bearer $LJ::STRIPE{api_key}|,
|
||
|
|
'Content-Type' => 'application/x-www-form-urlencoded',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
# checkout_url()
|
||
|
|
#
|
||
|
|
# this is simple, send them to the page for entering their credit card information
|
||
|
|
sub checkout_url {
|
||
|
|
my $self = $_[0];
|
||
|
|
|
||
|
|
# make sure that the cart contains something that costs something. since
|
||
|
|
# this check should have been done above, we die hardcore here.
|
||
|
|
my $cart = $self->cart;
|
||
|
|
die "Constraints not met: cart && cart->has_items && cart->total_cash > 0.00.\n"
|
||
|
|
unless $cart && $cart->has_items && $cart->total_cash > 0.00;
|
||
|
|
|
||
|
|
# and, just in case something terrible happens, make sure our state is good
|
||
|
|
die "Cart not in valid state!\n"
|
||
|
|
unless $cart->state == $DW::Shop::STATE_OPEN;
|
||
|
|
|
||
|
|
# Create cart items for Striping
|
||
|
|
my @items;
|
||
|
|
foreach my $item ( @{ $cart->items } ) {
|
||
|
|
push @items,
|
||
|
|
{
|
||
|
|
name => $item->name_text,
|
||
|
|
amount => $item->paid_cash * 100,
|
||
|
|
quantity => 1,
|
||
|
|
currency => 'usd',
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
# start a session for this user, then redirect and send them to the Stripe interface
|
||
|
|
# to actually complete the payment
|
||
|
|
my $res = _post(
|
||
|
|
'checkout/sessions',
|
||
|
|
{
|
||
|
|
cancel_url => "$LJ::SHOPROOT",
|
||
|
|
success_url => "$LJ::SHOPROOT/receipt?ordernum=" . $cart->ordernum,
|
||
|
|
payment_method_types => ['card'],
|
||
|
|
client_reference_id => $cart->id,
|
||
|
|
line_items => \@items,
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
if ( $res->is_success ) {
|
||
|
|
my $obj = from_json( $res->decoded_content );
|
||
|
|
$cart->state($DW::Shop::STATE_PEND_PAID);
|
||
|
|
$cart->paymentmethod_metadata( session_id => $obj->{id} );
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
confess 'Failed to start Stripe checkout process.';
|
||
|
|
}
|
||
|
|
|
||
|
|
# return URL to cc entry
|
||
|
|
return "$LJ::SHOPROOT/stripe-checkout?ordernum=" . $cart->ordernum;
|
||
|
|
}
|
||
|
|
|
||
|
|
# process an incoming webhook
|
||
|
|
sub process_webhook {
|
||
|
|
my ( $class, $event ) = @_;
|
||
|
|
|
||
|
|
if ( $event->{type} eq 'checkout.session.completed' ) {
|
||
|
|
my $cartid = $event->{data}{object}{client_reference_id};
|
||
|
|
return ( 400, 'Invalid client_reference_id (invalid/not provided).' )
|
||
|
|
unless defined $cartid;
|
||
|
|
$cartid += 0;
|
||
|
|
|
||
|
|
my $cart = DW::Shop::Cart->get_from_cartid($cartid);
|
||
|
|
return ( 400, 'Invalid client_reference_id (cart not found).' )
|
||
|
|
unless defined $cart;
|
||
|
|
|
||
|
|
my $engine = $class->new($cart);
|
||
|
|
return ( 500, 'Unable to build engine.' )
|
||
|
|
unless $engine;
|
||
|
|
|
||
|
|
# This event should only be fired when the cart has been paid, and in
|
||
|
|
# that case, we should move the cart along.
|
||
|
|
if ( $cart->state == $DW::Shop::STATE_PEND_PAID ) {
|
||
|
|
$cart->state($DW::Shop::STATE_PAID);
|
||
|
|
|
||
|
|
# TODO: What if this fails? do we need to refund the user?
|
||
|
|
$engine->try_capture_points;
|
||
|
|
}
|
||
|
|
|
||
|
|
return ( 200, 'I gotchu, Stripe. User is good!' );
|
||
|
|
}
|
||
|
|
|
||
|
|
return ( 400, 'Unsupported event.' );
|
||
|
|
}
|
||
|
|
|
||
|
|
# accessors
|
||
|
|
sub cart { $_[0]->{cart} }
|
||
|
|
|
||
|
|
1;
|