diff --git a/.gitignore b/.gitignore
index 78d27373..98eda1c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@ conf/config.ini.php
backup
*.gz
*.zip
+logs/*.log
#################
diff --git a/app/app.php b/app/app.php
index f732e8fe..4ee73bfa 100644
--- a/app/app.php
+++ b/app/app.php
@@ -16,7 +16,7 @@
require_once("../conf/config.ini.php");
require_once("include.php");
-$serverLog = (DEBUG) ? new \vhs\loggers\FileLogger(dirname(__FILE__) . "/server.log") : new \vhs\loggers\SilentLogger();
+$serverLog = (DEBUG) ? new \vhs\loggers\FileLogger(dirname(__FILE__) . "/../logs/server.log") : new \vhs\loggers\SilentLogger();
\vhs\web\HttpContext::Init(new \vhs\web\HttpServer(new \vhs\web\modules\HttpServerInfoModule("Nomos"), $serverLog));
diff --git a/app/contracts/IPaymentService1.php b/app/contracts/IPaymentService1.php
new file mode 100644
index 00000000..4e4ef850
--- /dev/null
+++ b/app/contracts/IPaymentService1.php
@@ -0,0 +1,19 @@
+code, $code)
);
}
+ /**
+ * @param $price
+ * @return Membership
+ */
public static function findForPriceLevel($price) {
$memberships = Membership::where(
Where::_And(
@@ -41,7 +49,7 @@ public static function findForPriceLevel($price) {
), OrderBy::Descending(MembershipSchema::Columns()->price), 1
);
- if (!is_null($memberships))
+ if (!is_null($memberships) && count($memberships) > 0)
return $memberships[0];
return null;
diff --git a/app/domain/Metric.php b/app/domain/Metric.php
index e7222057..9d2fe44f 100644
--- a/app/domain/Metric.php
+++ b/app/domain/Metric.php
@@ -5,8 +5,8 @@
namespace app\domain;
+use app\schema\PaymentSchema;
use app\schema\UserSchema;
-use app\schema\PaymentsSchema;
use app\domain\User;
use vhs\database\Database;
use vhs\database\On;
@@ -55,19 +55,19 @@ public static function NewMemberCount($start, $end) {
public static function TotalMemberCount($start, $end) {
$where = Where::_And(
Where::Equal(UserSchema::Columns()->active,"y"),
- Where::Equal(PaymentsSchema::Columns()->status, 1),
+ Where::Equal(PaymentSchema::Columns()->status, 1),
Where::_And(
- Where::GreaterEqual(PaymentsSchema::Columns()->date, date('Y-m-d 00:00:00', $start)),
- Where::Lesser(PaymentsSchema::Columns()->date, date('Y-m-d 00:00:00', $end))
+ Where::GreaterEqual(PaymentSchema::Columns()->date, date('Y-m-d 00:00:00', $start)),
+ Where::Lesser(PaymentSchema::Columns()->date, date('Y-m-d 00:00:00', $end))
)
);
$query = Query::count(UserSchema::Table(), $where);
$joinPayments = Join::Left(
- PaymentsSchema::Table,
+ PaymentSchema::Table,
On::Where(
- Where::Equal(UserSchema::Columns()->id,PaymentsSchema::Columns()->user_id)
+ Where::Equal(UserSchema::Columns()->id,PaymentSchema::Columns()->user_id)
)
);
$query->Join($joinPayments);
diff --git a/app/domain/Payment.php b/app/domain/Payment.php
new file mode 100644
index 00000000..e3aa513d
--- /dev/null
+++ b/app/domain/Payment.php
@@ -0,0 +1,37 @@
+txn_id, $txn_id)
+ )
+ );
+ }
+
+ public function validate(ValidationResults &$results) {
+
+ }
+}
\ No newline at end of file
diff --git a/app/domain/User.php b/app/domain/User.php
index aa208132..3b98a7f5 100644
--- a/app/domain/User.php
+++ b/app/domain/User.php
@@ -37,18 +37,31 @@ private function validateEmail(ValidationResults &$results) {
$results->add(new ValidationFailure("Invalid e-mail address"));
}
+ /**
+ * @param $username
+ * @return User[]
+ */
public static function findByUsername($username) {
return User::where(
Where::Equal(UserSchema::Columns()->username, $username)
);
}
+ /**
+ * @param $email
+ * @return User[]
+ */
public static function findByEmail($email) {
return User::where(
Where::Equal(UserSchema::Columns()->email, $email)
);
}
+ /**
+ * @param string|null $username
+ * @param string|null $email
+ * @return boolean
+ */
public static function exists($username = null, $email = null) {
$usernameWhere = Where::Equal(UserSchema::Columns()->username, $username);
$emailWhere = Where::Equal(UserSchema::Columns()->email, $email);
diff --git a/app/endpoints/web/PaymentService1.svc.php b/app/endpoints/web/PaymentService1.svc.php
new file mode 100644
index 00000000..b2cf3f30
--- /dev/null
+++ b/app/endpoints/web/PaymentService1.svc.php
@@ -0,0 +1,14 @@
+logger = &$logger;
+ Payment::onAnyCreated([$this, "paymentCreated"]);
+ }
+
+ private function log($message)
+ {
+ $this->logger->log("[PaymentMonitor] {$message}");
+ }
+
+ public function paymentCreated($args)
+ {
+ $emailService = new EmailService();
+
+ $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
+ $domainName = $_SERVER['HTTP_HOST'].'/';
+
+ $host = $protocol.$domainName;
+
+ /** @var Payment $payment */
+ $payment = $args[0];
+
+ if ($payment->status == 1)
+ return;
+
+ /** @var User $user */
+ $user = null;
+ $users = User::findByEmail($payment->payer_email);
+
+ if (count($users) > 1) {
+ $this->log("Found more than one user for email '{$payment->payer_email}' unable to process payment");
+ return;
+ } elseif (count($users) == 1) {
+ $user = $users[0];
+ }
+
+ //TODO get membership type via item_name/item_number from payment record
+ /** @var Membership $membership */
+ $membership = Membership::findForPriceLevel($payment->rate_amount);
+ if (is_null($membership)) {
+ $memberships = Membership::findByCode("member");
+
+ if (!is_null($memberships) && count($memberships) == 1) {
+ $membership = $memberships[0];
+ } else {
+ $this->log("Missing membership type 'member'. Unable to process payment.");
+ return;
+ }
+ }
+
+ $userService = new UserService();
+
+ if (is_null($user)) { //new user
+ try {
+ $user = $userService->Create(
+ $payment->payer_email,
+ PasswordUtil::generate(),
+ $payment->payer_email,
+ $payment->payer_fname,
+ $payment->payer_lname,
+ $membership->id
+ );
+ } catch (\Exception $ex) {
+ //this shouldn't happen... we should've found the user by email otherwise...
+ $this->log($ex->getMessage());
+ return;
+ }
+
+ $emailService->EmailUser(
+ [ 'email' => NOMOS_FROM_EMAIL ],
+ '[Nomos] New User Created!',
+ 'admin_newuser',
+ [
+ 'email' => $payment->payer_email,
+ 'fname' => $payment->payer_fname,
+ 'lname' => $payment->payer_lname
+ ]
+ );
+
+ } else {
+ if ($user->membership_id != $membership->id)
+ $userService->UpdateMembership($user->id, $membership->id);
+ }
+
+ $expiry = new DateTime($payment->date);
+ $expiry->add(new \DateInterval("P1M1W")); //add 1 month with a 1 week grace period
+
+ $user->mem_expire = $expiry->format("Y-m-d H:i:s");
+
+ $user->active = "y";
+
+ $user->save();
+
+ $payment->user_id = $user->id;
+ $payment->membership_id = $membership->id;
+ $payment->status = 1; //processed
+ $payment->save();
+
+ $emailService->EmailUser(
+ [ 'email' => NOMOS_FROM_EMAIL ],
+ '[Nomos] User payment made!',
+ 'admin_payment',
+ [
+ 'email' => $payment->payer_email,
+ 'fname' => $payment->payer_fname,
+ 'lname' => $payment->payer_lname,
+ 'amount' => $payment->rate_amount,
+ 'pp' => $payment->pp
+ ]
+ );
+
+ $emailService->EmailUser(
+ $user,
+ 'VHS Membership Payment Received!',
+ 'payment',
+ [
+ 'host' => $host,
+ 'fname' => $user->fname,
+ ]
+ );
+ }
+}
diff --git a/app/monitors/PaypalIpnMonitor.php b/app/monitors/PaypalIpnMonitor.php
index be840525..2da8e84b 100644
--- a/app/monitors/PaypalIpnMonitor.php
+++ b/app/monitors/PaypalIpnMonitor.php
@@ -10,6 +10,7 @@
use app\domain\Ipn;
use app\domain\Membership;
+use app\domain\Payment;
use app\domain\User;
use app\security\PasswordUtil;
use app\services\EmailService;
@@ -19,6 +20,7 @@
class PaypalIpnMonitor extends Monitor {
+ /** @var Logger */
private $logger;
public function Init(Logger &$logger = null) {
@@ -28,12 +30,10 @@ public function Init(Logger &$logger = null) {
public function handleCreated($args)
{
+ /** @var Ipn $ipn */
$ipn = $args[0];
if ($ipn->validation == "VERIFIED" && $ipn->payment_status == "COMPLETED") {
- //todo maybe do a hash or record the id of this $ipn record to mark it completed
- //todo create transaction, update user, etc
-
$this->logger->log("We have a valid $ipn record, create a transaction, update users, etc");
$kvp = explode("&", $ipn->raw);
@@ -48,69 +48,32 @@ public function handleCreated($args)
$raw[$pair[0]] = $pair[1];
}
- /* todo
- the bare minimum is to email the admin (set to my email right now, but we should fix config -
- I'll do an issue report now), and create an account on new user
- so... get an IPN, compare paypal email with existing, if new, create new account, activate as
- member/friend, send admin email
- if existing, set type to member/friend/keyholder(if allowed), extend expiry, activate user account,
- email admin
- a failed IPN should also email admin
- */
-
- $isExistingUser = false;
-
- $user = User::findByEmail($ipn->payer_email);
-
- $member = Membership::findByCode("member");
-
- $userService = new UserService();
-
- if (is_null($user)) {
- $password = PasswordUtil::hash(
- substr(
- str_shuffle(
- str_repeat('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',16)
- ),0,16
- )
- );
-
- $fname = (in_array("first_name", $raw)) ? $raw["first_name"] : $ipn->payer_email;
- $lname = (in_array("last_name", $raw)) ? $raw["last_name"] : $ipn->payer_email;
-
- try {
- $user = $userService->Create(
- $ipn->payer_email,
- $password,
- $ipn->payer_email,
- $fname,
- $lname,
- $member->id
- );
- } catch (\Exception $ex) {
- //this shouldn't happen... we should've found the user by email otherwise...
- $this->logger->log($ex->getMessage());
- }
-
- //email admin
- } else {
- $newMembership = Membership::findForPriceLevel($ipn->payment_amount);
-
- if ($user->membership_id != $newMembership->id)
- {
- //todo need to do a permission check on the user prob..
- $userService->UpdateMembership($user->id, $newMembership->id);
- }
-
- $user->active = "y";
-
- //todo need to calc the new expiry date.
- //$user->mem_expire = "";
-
- $user->save();
-
- //email user & admin
+ //todo this should prob be a paypal transaction id
+ if (Payment::exists($ipn->id))
+ {
+ $this->logger->log("Payment record already exists for this IPN transaction.. odd");
+ return;
}
+
+ $payment = new Payment();
+
+ $payment->txn_id = $ipn->id; //todo this should prob be a paypal transaction id
+
+ $payment->rate_amount = $ipn->payment_amount;
+ $payment->currency = $ipn->payment_currency;
+ $payment->pp = "PayPal";
+
+ $payment->status = 0; //we haven't processed the payment internally yet
+
+ $payment->payer_email = $ipn->payer_email;
+ $payment->payer_fname = (in_array("first_name", $raw)) ? $raw["first_name"] : $ipn->payer_email;
+ $payment->payer_lname = (in_array("last_name", $raw)) ? $raw["last_name"] : $ipn->payer_email;
+
+ $payment->item_number = $ipn->item_number;
+ $payment->item_name = $ipn->item_name;
+ $payment->date = $ipn->created; //todo this is prob wrong, we should get a paypal date
+
+ $payment->save();
}
}
diff --git a/app/schema/IpnSchema.php b/app/schema/IpnSchema.php
index 989b4442..cfc3aa84 100644
--- a/app/schema/IpnSchema.php
+++ b/app/schema/IpnSchema.php
@@ -25,6 +25,8 @@ public static function init() {
$table->addColumn("payment_amount", Type::String(false, "", 255));
$table->addColumn("payment_currency", Type::String(false, "", 255));
$table->addColumn("payer_email", Type::String(false, "", 255));
+ $table->addColumn("item_name", Type::String(false, "", 255));
+ $table->addColumn("item_number", Type::String(false, "", 255));
$table->addColumn("raw", Type::String(false, "", 255));
$table->setConstraints(
diff --git a/app/schema/PaymentSchema.php b/app/schema/PaymentSchema.php
new file mode 100644
index 00000000..5f3cf4f9
--- /dev/null
+++ b/app/schema/PaymentSchema.php
@@ -0,0 +1,48 @@
+addColumn("id", Type::Int(false, 0));
+ $table->addColumn("txn_id", Type::String(false, "", 100)); //txn_id
+ $table->addColumn("membership_id", Type::Int(true, 0));
+ $table->addColumn("user_id", Type::Int(true, 0));
+ $table->addColumn("payer_email", Type::String(true, null, 255));
+ $table->addColumn("payer_fname", Type::String(true, null, 255));
+ $table->addColumn("payer_lname", Type::String(true, null, 255));
+ $table->addColumn("rate_amount", Type::String(false, "", 255));
+ $table->addColumn("currency", Type::String(true, null, 4));
+ $table->addColumn("date", Type::DateTime(false, date("Y-m-d H:i:s")));
+ $table->addColumn("pp", Type::Enum("PayPal", "MoneyBookers"));
+ $table->addColumn("ip", Type::String(true, null, 20));
+ $table->addColumn("status", Type::Int(false, 0)); // 1==completed, anything else is "pending"
+ $table->addColumn("item_name", Type::String(true, null, 255));
+ $table->addColumn("item_number", Type::String(true, null, 255));
+
+ $table->setConstraints(
+ Constraint::PrimaryKey($table->columns->id),
+ Constraint::ForeignKey($table->columns->membership_id, MembershipSchema::Table(), MembershipSchema::Columns()->id),
+ Constraint::ForeignKey($table->columns->user_id, UserSchema::Table(), UserSchema::Columns()->id)
+ );
+
+ return $table;
+ }
+}
diff --git a/app/schema/PaymentsSchema.php b/app/schema/PaymentsSchema.php
deleted file mode 100644
index e44295b7..00000000
--- a/app/schema/PaymentsSchema.php
+++ /dev/null
@@ -1,39 +0,0 @@
-addColumn("id", Type::Int(false, 0));
- $table->addColumn("transactionref", Type::String(false, "", 100)); //txn_id
- $table->addColumn("membership_id", Type::Int(false, 0));
- $table->addColumn("rate_amount", Type::String(false, "", 255));
- $table->addColumn("currency", Type::String(false, "", 3));
- $table->addColumn("date", Type::DateTime(true, date("Y-m-d H:i:s")));
- $table->addColumn("paymentprocessor", Type::String(false, "", 255));
- $table->addColumn("ip", Type::String(false, "", 20));
- $table->addColumn("status", Type::Int(false, 0));
-
-
- $table->setConstraints(
- Constraint::PrimaryKey($table->columns->id)
- );
-
- return $table;
- }
-}
diff --git a/app/security/PasswordUtil.php b/app/security/PasswordUtil.php
index 3642d89e..458c83e3 100644
--- a/app/security/PasswordUtil.php
+++ b/app/security/PasswordUtil.php
@@ -16,6 +16,16 @@
class PasswordUtil {
+ public static function generate() {
+ return self::hash(
+ substr(
+ str_shuffle(
+ str_repeat('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',16)
+ ),0,16
+ )
+ );
+ }
+
public static function hash($password) {
return self::password_hash(sha1($password), PASSWORD_BCRYPT);
}
diff --git a/app/services/EmailService.php b/app/services/EmailService.php
index 32bfd74f..d0b7c25a 100644
--- a/app/services/EmailService.php
+++ b/app/services/EmailService.php
@@ -29,7 +29,7 @@ public function EmailUser($user, $subject, $tmpl, $context) {
));
$client->sendEmail(array(
- 'Source' => MMP_FROM_EMAIL,
+ 'Source' => NOMOS_FROM_EMAIL,
'Destination' => array(
'ToAddresses' => array($user->email),
),
diff --git a/app/services/PaymentService.php b/app/services/PaymentService.php
new file mode 100644
index 00000000..980df73d
--- /dev/null
+++ b/app/services/PaymentService.php
@@ -0,0 +1,18 @@
+email = $email;
$user->fname = $fname;
$user->lname = $lname;
- $user->active = "y";//"t"; //TODO send email activation
+ $user->active = "t";
+ $user->token = bin2hex(openssl_random_pseudo_bytes(8));
$user->save();
+ $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
+ $domainName = $_SERVER['HTTP_HOST'].'/';
+
+ $emailService = new EmailService();
+
+ //todo finish this template and add the supporting services to actual active the account
+ $emailService->EmailUser($user, 'VHS Nomos Account Activation', 'welcome', [
+ 'token' => $user->token,
+ 'host' => $protocol.$domainName
+ ]);
+
return $user;
}
@@ -125,6 +137,7 @@ public function Create($username, $password, $email, $fname, $lname, $membership
$user->fname = $fname;
$user->lname = $lname;
$user->active = "t";
+ $user->token = bin2hex(openssl_random_pseudo_bytes(8));
$user->save();
@@ -132,6 +145,17 @@ public function Create($username, $password, $email, $fname, $lname, $membership
$this->UpdateMembership($user->id, $membershipid);
} catch(\Exception $ex) {}
+ $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
+ $domainName = $_SERVER['HTTP_HOST'].'/';
+
+ $emailService = new EmailService();
+
+ //todo finish this template and add the supporting services to actual active the account
+ $emailService->EmailUser($user, 'VHS Nomos Account Activation', 'welcome', [
+ 'token' => $user->token,
+ 'host' => $protocol.$domainName
+ ]);
+
return $user;
}
@@ -213,6 +237,9 @@ public function UpdateMembership($userid, $membershipid) {
throw new \Exception("Invalid user or membership type");
}
+ if ($membership->code == "key-holder" && !$user->privileges->contains(Privilege::findByCode("vetted")))
+ return;
+
$user->membership = $membership;
$user->save();
diff --git a/conf/config.ini.php.template b/conf/config.ini.php.template
index 45c85b85..846d5cfb 100644
--- a/conf/config.ini.php.template
+++ b/conf/config.ini.php.template
@@ -9,7 +9,7 @@
define('DB_PASS', 'password');
define('DB_DATABASE', 'vhs_membership');
- define('MMP_FROM_EMAIL', 'membership@hackspace.ca');
+ define('NOMOS_FROM_EMAIL', 'membership@hackspace.ca');
// @funvill has these, ask him for the values
// SES is used to send e-mails
diff --git a/conf/nginx-vhost-vagrant.conf b/conf/nginx-vhost-vagrant.conf
index 24a5ea9f..91221b7d 100644
--- a/conf/nginx-vhost-vagrant.conf
+++ b/conf/nginx-vhost-vagrant.conf
@@ -6,15 +6,6 @@ server {
membership.hackspace.ca
192.168.38.10;
- location ~ (/|\.php)$ {
- include fastcgi_params;
- fastcgi_split_path_info ^(.+\.php)(/.+)$;
- fastcgi_index index.php;
- fastcgi_param SCRIPT_FILENAME $app_root/scripts$fastcgi_script_name;
- fastcgi_pass unix:/var/run/php5-fpm.sock;
- fastcgi_buffers 16 16k;
- fastcgi_buffer_size 32k;
- }
location ~ (/services/) {
include fastcgi_params;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
diff --git a/conf/nginx-vhost-windows.conf b/conf/nginx-vhost-windows.conf
index 55dff580..47477409 100644
--- a/conf/nginx-vhost-windows.conf
+++ b/conf/nginx-vhost-windows.conf
@@ -15,17 +15,6 @@ server {
fastcgi_param SCRIPT_FILENAME $app_root$fastcgi_script_name;
include fastcgi_params;
}
-
- location ~ (/|\.php)$ {
- #fastcgi_intercept_errors on;
- #try_files $uri =404;
- fastcgi_split_path_info ^(.+\.php)(/.+)$;
- fastcgi_read_timeout 300;
- fastcgi_pass 127.0.0.1:9000;
- fastcgi_index index.php;
- fastcgi_param SCRIPT_FILENAME $app_root/scripts$fastcgi_script_name;
- include fastcgi_params;
- }
location ~ (/services/) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
diff --git a/conf/nginx.conf b/conf/nginx.conf
index 9939fa03..24343ee6 100644
--- a/conf/nginx.conf
+++ b/conf/nginx.conf
@@ -4,18 +4,6 @@ server {
listen 80;
server_name membership.hackspace.ca;
- location ~ (/|\.php)$ {
- include fastcgi_params;
- fastcgi_split_path_info ^(.+\.php)(/.+)$;
- # # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
-
- # # With php5-cgi alone:
- # fastcgi_pass 127.0.0.1:9000;
- # With php5-fpm:
- fastcgi_index index.php;
- fastcgi_param SCRIPT_FILENAME $app_root/scripts$fastcgi_script_name;
- fastcgi_pass 127.0.0.1:9000;
- }
location ~ (/services/) {
include fastcgi_params;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
diff --git a/logs/EMPTY b/logs/EMPTY
new file mode 100644
index 00000000..1ec6b8a6
--- /dev/null
+++ b/logs/EMPTY
@@ -0,0 +1 @@
+EMPTY
\ No newline at end of file
diff --git a/migrations/13/payments.sql b/migrations/13/payments.sql
new file mode 100644
index 00000000..3ed9fa39
--- /dev/null
+++ b/migrations/13/payments.sql
@@ -0,0 +1,6 @@
+ALTER TABLE `payments` ADD COLUMN `payer_email` VARCHAR(255) NULL;
+ALTER TABLE `payments` ADD COLUMN `payer_fname` VARCHAR(255) NULL;
+ALTER TABLE `payments` ADD COLUMN `payer_lname` VARCHAR(255) NULL;
+
+ALTER TABLE `payments` MODIFY COLUMN `user_id` INT(11) NULL;
+ALTER TABLE `payments` MODIFY COLUMN `membership_id` INT(11) NULL;
diff --git a/vhs/email/admin_newuser.html b/vhs/email/admin_newuser.html
new file mode 100644
index 00000000..2d013d4c
--- /dev/null
+++ b/vhs/email/admin_newuser.html
@@ -0,0 +1,6 @@
+New user added!
+
+{{fname}} {{lname}}
+
+Email: {{email}}
+
diff --git a/vhs/email/admin_newuser.txt b/vhs/email/admin_newuser.txt
new file mode 100644
index 00000000..ebef5c78
--- /dev/null
+++ b/vhs/email/admin_newuser.txt
@@ -0,0 +1,5 @@
+New user added!
+
+{{fname}} {{lname}}
+Email: {{email}}
+
diff --git a/vhs/email/admin_payment.html b/vhs/email/admin_payment.html
new file mode 100644
index 00000000..65404b69
--- /dev/null
+++ b/vhs/email/admin_payment.html
@@ -0,0 +1 @@
+{{fname}} {{lname}}, {{email}}, made payment of {{amount}} via {{pp}}.
diff --git a/vhs/email/admin_payment.txt b/vhs/email/admin_payment.txt
new file mode 100644
index 00000000..65404b69
--- /dev/null
+++ b/vhs/email/admin_payment.txt
@@ -0,0 +1 @@
+{{fname}} {{lname}}, {{email}}, made payment of {{amount}} via {{pp}}.
diff --git a/vhs/email/payment.html b/vhs/email/payment.html
new file mode 100644
index 00000000..1dfa4328
--- /dev/null
+++ b/vhs/email/payment.html
@@ -0,0 +1,8 @@
+Dear {{fname}},
+
+Thank you for your payment!
+
+- Vancouver Hackspace Society
+
+{{host}}
+
\ No newline at end of file
diff --git a/vhs/email/payment.txt b/vhs/email/payment.txt
new file mode 100644
index 00000000..def4ab4a
--- /dev/null
+++ b/vhs/email/payment.txt
@@ -0,0 +1,7 @@
+Dear {{fname}},
+
+Thank you for your payment!
+
+- Vancouver Hackspace Society
+
+{{host}}
diff --git a/web/admin/admin.html b/web/admin/admin.html
index 16422deb..757dfae1 100644
--- a/web/admin/admin.html
+++ b/web/admin/admin.html
@@ -28,9 +28,6 @@