Add code from gists 12311[56]
t0m [Sun, 7 Jun 2009 01:17:19 +0000 (02:17 +0100)]
t/lib/TestComplexApp/Controller/PaymentProvider.pm [new file with mode: 0644]
t/lib/TestComplexApp/Model/PaymentProvider.pm [new file with mode: 0644]

diff --git a/t/lib/TestComplexApp/Controller/PaymentProvider.pm b/t/lib/TestComplexApp/Controller/PaymentProvider.pm
new file mode 100644 (file)
index 0000000..806e5e2
--- /dev/null
@@ -0,0 +1,116 @@
+package TestComplexApp::Controller::PaymentProvider;
+use Moose;
+use Moose::Meta::Class;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; };
+
+=head1 NAME
+
+PaymentApp::Controller::PaymentProvider - Provider Controller factory
+
+=head1 DESCRIPTION
+
+This module creates dynamic Catalyst controllers based on
+configuration. It's intended for use with Catalyst::Engine::Stomp,
+where each controller namespace corresponds to a message queue
+subscription.
+
+The generated controllers expect Model classes to be defined for the
+configured payment providers, as configured by
+PaymentApp::Model::PaymentProvider - and in fact we borrow that
+module's config.
+
+We create one controller for each configured payment provider, for
+each configured environment. The generated namespace is based on both
+these names, so you end up with queues like "live_datacash". 
+
+The relevant configuration comes from Catalyst, and should resemble
+this:
+
+  Model::PaymentProvider:
+    providers:
+      - PaymentProvider::Datacash
+      - PaymentProvider::Cybersource
+      - PaymentProvider::Null
+
+  Controller::PaymentProvider:
+    environments:
+      - qa
+      - test
+      - custqa
+      - uat
+
+=head1 TODO
+
+We're currently hardcoding the list of methods which form the
+interface to a payment processor. This will change - we will require
+that a payment provider class "does" a Moose role, and that role will
+indicate the methods we should consider dispatchable. 
+
+=head1 SEE ALSO
+
+PaymentApp::Model::PaymentProvider - this module's partner in crime.
+
+=cut
+
+__PACKAGE__->config( namespace => '' );
+
+sub COMPONENT {
+       my $self = shift->next::method(@_);
+       
+       # Sneakily borrow our corresponding model's config for providers
+       my $model_config = $self->_app->config->{'Model::PaymentProvider'};
+       my $provider_classes = $model_config->{providers};
+
+       # Environment list is from our own config. 
+       my $environments = $self->{environments};
+       
+       # For each configured provider/environment combination, create
+       # a Controller class which dispatches a set of methods to the
+       # provider's Model.
+       for my $provider_class (@$provider_classes) {
+               for my $environment (@$environments) {
+
+                       # Compose the name of the Controller
+                       my $env_class = ucfirst($environment);
+                       my $provider_controller_name = "Controller::${provider_class}::${env_class}";
+
+                       # Compose its namespace - i.e. the STOMP queue name
+                       my $namespace = $environment . '_' . $provider_class->name;
+                       $self->_app->config( $provider_controller_name => { namespace => $namespace } );
+
+                       # Create the class with Moose
+                       my $provider = Moose::Meta::Class->create("PaymentApp::${provider_controller_name}");
+                       $provider->superclasses('PaymentApp::ControllerBase::Message');
+
+                       # XXX from interface role, not static list here!
+                       my @dispatchable_methods = qw/ payment_auth_request
+                                                      payment_settle_request
+                                                      payment_refund_request
+                                                    /;
+                       
+                       # Attach a method for each action, making sure
+                       # to apply the right method attribute to turn
+                       # these into actions
+                       for my $method_name (@dispatchable_methods) {
+                               my $sub = sub {
+                                       my ($self, $c, $message) = @_;
+                                       my $provider_model = $c->model($provider_class);
+                                       my $response = $provider_model->$method_name($message);
+                                       $c->stash->{response} = $response;
+                               };
+                               
+                               # Why doesn't add_method return the method?
+                               $provider->add_method($method_name, $sub);
+                               my $method = $provider->get_method($method_name);
+                               $provider->register_method_attributes($method->body, ['Local']);
+                       }
+               }
+       }
+
+       return $self;
+}
+       
+__PACKAGE__->meta->make_immutable;
+
diff --git a/t/lib/TestComplexApp/Model/PaymentProvider.pm b/t/lib/TestComplexApp/Model/PaymentProvider.pm
new file mode 100644 (file)
index 0000000..1f1038a
--- /dev/null
@@ -0,0 +1,100 @@
+package TestComplexApp::Model::PaymentProvider;
+use Moose;
+use Moose::Meta::Class;
+use namespace::autoclean;
+
+extends 'Catalyst::Model';
+
+=head1 NAME
+
+PaymentApp::Model::PaymentProvider - Provider Model factory
+
+=head1 DESCRIPTION 
+
+This module creates dynamic Catalyst Models based on configured
+payment providers and the requested payment profile. 
+
+The Model instance is created at request time, not at setup time, and
+so we can inspect the message to see which enterprise and profile is
+required. We then load that profile using the ProfileDB Model, and
+instantiate the Model. The corresponding generated controller then
+invokes the appropriate method on this Model.
+
+We create a Model class per configured payment provider, and we
+require this configuration:
+
+  Model::PaymentProvider:
+    providers:
+      - PaymentProvider::Datacash
+      - PaymentProvider::Cybersource
+      - PaymentProvider::Null
+
+The Model instances may be accessed like so:
+
+  $c->model('PaymentProvider::Datacash')
+
+with the enterprise and profile names in the stash.
+
+=head1 TODO
+
+More graceful handling of missing enterprise and/or profile. We should
+do something other than just not return a model. 
+
+=head1 SEE ALSO
+
+PaymentApp::Controller::PaymentProvider - this module's partner in crime.
+
+=cut
+
+sub COMPONENT {
+       my $self = shift->next::method(@_);
+       
+       my $provider_classes = $self->{providers};
+
+       for my $provider_class (@$provider_classes) {
+               eval "require $provider_class";
+               if ($@) {
+                       die "loading $provider_class: $@";
+               }
+               
+               my $provider_model_name = "PaymentApp::Model::${provider_class}";
+               my $provider = Moose::Meta::Class->create($provider_model_name);
+               $provider->add_method('ACCEPT_CONTEXT', 
+                                     sub {
+                                             my ($model, $c) = @_;
+                                             my $profile = $self->load_profile($c);
+                                             my $provider_model = $provider_class->new($profile);
+                                             return $provider_model;
+                                     });
+       }
+
+       return $self;
+}
+
+# Load the given profile and instantiate the provider
+sub load_profile {
+       my ($self, $c) = @_;
+
+       my $profile_name = $c->stash->{profile};
+       my $enterprise_name = $c->stash->{enterprise};
+       
+       my $enterprise = $c->model('ProfileDB::Enterprise')->single(
+               {name => $enterprise_name}
+       );
+       my $profile = $c->model('ProfileDB::Profile')->single(
+               {name => $profile_name, enterprise_id => $enterprise->id}
+       );
+       
+       my $profile_data = {};
+       my $set_rs = $c->model('ProfileDB::ProfileSetting')->search(
+               {profile_id => $profile->id}
+       );
+       while (my $set = $set_rs->next) {
+               $profile_data->{$set->name} = $set->value;
+       }
+       
+       return $profile_data;
+}
+       
+1;
+