<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[My Journey]]></title><description><![CDATA[My Journey]]></description><link>https://davidmason.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ac4f7f48571823aa9acce4d/ed6e76c3-e189-4433-9861-1fb360a02de0.jpg</url><title>My Journey</title><link>https://davidmason.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 31 Oct 2026 08:12:19 GMT</lastBuildDate><atom:link href="https://davidmason.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Poo Tracker]]></title><description><![CDATA[I wanted to create a particular app which would incorporate a Serverless front end but also have the ability to display previous logs. I opted to create a…]]></description><link>https://davidmason.hashnode.dev/poo-tracker</link><guid isPermaLink="true">https://davidmason.hashnode.dev/poo-tracker</guid><dc:creator><![CDATA[David]]></dc:creator><pubDate>Mon, 19 Oct 2026 12:51:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/08264c48-ad49-407a-889f-059f7647ddc4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I wanted to create a particular app which would incorporate a Serverless front end but also have the ability to display previous logs.</p>
<p>I opted to create a rather crude form that allowed someone living with a Bowel condition to log their motions including pictures if relevant.</p>
<p>In an ideal world, the user would login, provide new information and be able to see previous records. I was hoping to not lean on AWS Amplify for this project as I wanted to use a Secure S3 Bucket tied in with AWS Cloudfront and Cognito. Sadly Cloudfront is not part of the Entry tier of AWS so I opted not to push for that at this stage.</p>
<p>I will break this down by the Service</p>
<p><strong>AWS S3</strong></p>
<p>We have 2 different S3 Buckets operating here.</p>
<ol>
<li><p><strong>Frontend Hosting</strong></p>
<ul>
<li>This was used to host the site but had a JSON Policy of s3:GetObject to *, which allowed public access. This is ideal for unencrypted testing but not much else in the real world. Ideally this would sit behind Cloudfront and only be accessed via Cognito Login. I will do this on a separate Project.</li>
</ul>
</li>
<li><p><strong>Image Storage</strong></p>
<ul>
<li>Public Access was blocked on this, and only the Lambda function had the IAM permission to trigger the storage.</li>
</ul>
</li>
</ol>
<p><strong>AWS DynamoDB</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/65a1ab78-3e17-4c94-bfee-6bb196629575.png" alt="" style="display:block;margin:0 auto" />

<p>This Database was chosen primarily for the Free tier access but also as we didn't need a relational database for such a quick project.</p>
<p>UserId is used to group the data and timestamp allows for the entries to be sorted.</p>
<p><strong>AWS IAM (Identity and Access Management)</strong></p>
<p>As mentioned, Services can't inherently talk to each other in AWS, so Policies were added to ensure this could happen. <code>AmazonDynamoDBFullAccess</code>, <code>AmazonS3FullAccess</code>, and <code>AmazonSNSFullAccess</code>.</p>
<p>In a Full Production Software, we would use Least Privilege to set these up.</p>
<p><strong>AWS Lambda</strong></p>
<p>Like S3, 2 branches of Lambda occur;</p>
<ol>
<li><p><strong>ProcessBristolSubmission</strong></p>
<ul>
<li><p>This allows image data to be put into the S3 bucket, itemises this on DynamoDB and then sends an Alert on SNS.</p>
</li>
<li><p>Note that User ID is set to 'test-user-001' for now until Cognito is set up.</p>
<pre><code class="language-python">import json
import boto3
import base64
import uuid
from datetime import datetime

# 1. Initialize Boto3 clients (The magic wands)
dynamodb = boto3.resource('dynamodb')
s3_client = boto3.client('s3')
sns_client = boto3.client('sns')

# 2. Hardcode your specific resources HERE
BUCKET_NAME = 'bristol-health-submissions-yourname-2026'
SNS_TOPIC_ARN = 'arn:aws:sns:eu-west-2:446503125379:BristolAlerts'
TABLE_NAME = 'BristolSubmissions'

def lambda_handler(event, context):
    try:
        # 3. Parse the data coming from API Gateway / Frontend
        body = json.loads(event['body'])
        
        bristol_scale = body.get('bristol_scale')
        pain_level = body.get('pain_level')
        blood_present = body.get('blood_present', False)
        image_base64 = body.get('image_data') # The photo file sent as text
        
        # We will use a dummy user ID until we set up Cognito later
        user_id = 'test-user-001'
        timestamp = datetime.utcnow().isoformat()
        image_url = "No image uploaded"
        
        # 4. If there is a photo, save it to S3
        if image_base64:
            image_name = f"{user_id}-{uuid.uuid4()}.jpg"
            image_bytes = base64.b64decode(image_base64)
            
            s3_client.put_object(
                Bucket=BUCKET_NAME,
                Key=image_name,
                Body=image_bytes,
                ContentType='image/jpeg'
            )
            image_url = f"s3://{BUCKET_NAME}/{image_name}"

        # 5. Save the text data to DynamoDB
        table = dynamodb.Table(TABLE_NAME)
        table.put_item(
            Item={
                'UserId': user_id,
                'Timestamp': timestamp,
                'BristolScale': bristol_scale,
                'PainLevel': pain_level,
                'BloodPresent': blood_present,
                'ImageUrl': image_url
            }
        )

        # 6. Send the email via SNS
        sns_client.publish(
            TopicArn=SNS_TOPIC_ARN,
            Subject='New Health Submission Logged',
            Message=f"A new submission was saved.\nBristol Scale: {bristol_scale}\nPain Level: {pain_level}\nBlood Present: {blood_present}"
        )

        # 7. Reply to the frontend website
        return {
            'statusCode': 200,
            # CORS headers are required so your web browser doesn't block the response
            'headers': {
                'Access-Control-Allow-Origin': '*', 
                'Access-Control-Allow-Headers': 'Content-Type',
            },
            'body': json.dumps({'message': 'Submission successful!'})
        }

    except Exception as e:
        # If anything goes wrong, tell the frontend
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
        }
</code></pre>
</li>
</ul>
</li>
<li><p><strong>GetBristolHistory</strong></p>
<ul>
<li>Using 'Query' we bring back the submissions of 'test-user-001' into the Form view</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/428d4be0-0269-4b0a-bff5-08a1f8a69ce7.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-python">import json
import boto3
from boto3.dynamodb.conditions import Key
import decimal

dynamodb = boto3.resource('dynamodb')
TABLE_NAME = 'BristolSubmissions'

# Helper class to convert a DynamoDB item to JSON.
class DecimalEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, decimal.Decimal):
            if o % 1 &gt; 0:
                return float(o)
            else:
                return int(o)
        return super(DecimalEncoder, self).default(o)

def lambda_handler(event, context):
    try:
        table = dynamodb.Table(TABLE_NAME)
        
        # We use the same dummy user ID we used to save the data
        user_id = 'test-user-001'
        
        # This is a DynamoDB Query. It instantly finds the "neighborhood" 
        # for this specific user, rather than scanning the whole database.
        response = table.query(
            KeyConditionExpression=Key('UserId').eq(user_id)
        )
        
        items = response.get('Items', [])
        
        # Sort the items so the newest submission is at the top of the table
        items.sort(key=lambda x: x['Timestamp'], reverse=True)

        return {
            'statusCode': 200,
            # CORS headers to allow your S3 website to fetch the data
            'headers': {
                'Access-Control-Allow-Origin': '*',
                'Access-Control-Allow-Headers': 'Content-Type',
            },
            # We use the DecimalEncoder to fix a common Boto3 bug
            'body': json.dumps({'history': items}, cls=DecimalEncoder)
        }

    except Exception as e:
        return {
            'statusCode': 500,
            'headers': {
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({'error': str(e)})
        }
</code></pre>
</li>
</ol>
<p>This is also my first knowledge of boto3, which is the official AWS Kit for Python, more to come on this I'm sure.</p>
<p><strong>AWS API Gateway</strong></p>
<p>This involves two routes;</p>
<p>Post /submit - Involved withe Form Submission</p>
<p>Post /history - Involved with the History View</p>
<p>CORS was set to *, and content-type allowed the methods of GET, POST and OPTIONS.</p>
<p><strong>AWS SNS (Simple Notification Service)</strong></p>
<p>I created 'BristolAlerts' which emailed any notification to me.</p>
<h2>Roadblock</h2>
<p>I was hoping to create a login using Cognito and a secure frontend using Cloudfront (to help deter DDOS) but Cloudfront is not available to me just yet.</p>
<h2>What's Next?</h2>
<p>I would like to pad this out further with some different features;</p>
<ol>
<li><p>Create a Login Page for the users so the form is not Public</p>
</li>
<li><p>Have the ability for the User to see their individual records and the photos attached to them</p>
</li>
<li><p>Create an Admin View</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Randomness]]></title><description><![CDATA[Understanding Math.random and Math.floor]]></description><link>https://davidmason.hashnode.dev/randomness</link><guid isPermaLink="true">https://davidmason.hashnode.dev/randomness</guid><dc:creator><![CDATA[David]]></dc:creator><pubDate>Tue, 13 Oct 2026 12:16:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/e4350c7e-5a91-4d6c-ad45-cbfe50466808.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As my journey to being a Web Developer continues, I have carried on my Javascript learnings via Scrimba.</p>
<p>Today I learnt about <strong>Math.random and Math.floor</strong> and how they work.</p>
<h2>Math.random</h2>
<p>If prompted, Math.random will give you a random (this in itself can be argued) number between 0 and 1. So it might be 0.2525357 or 0.9234 etc.</p>
<p>So how would we get a random number between 1 and 10? Well could start with this. This gives us a random number from 0 to 9, and then we +1 so we include 10 and exclude the 0.</p>
<pre><code class="language-python">let random = (Math.random()*10)+1
console.log (random)
</code></pre>
<h2>Math.floor</h2>
<p>This works well but it still decimal places in the way. If we apply Math.floor, then this will effectively snip off the decimal places.</p>
<pre><code class="language-python">let random = Math.floor((Math.random()*10)+1)
console.log (random)
</code></pre>
<p>This will now give us a random number between 1 and 10.</p>
]]></content:encoded></item><item><title><![CDATA[Creating a Photo Geolocation Tool]]></title><description><![CDATA[A friend of mine had an idea for an outdoor tracking app based on a submitted Photo. I thought this was a great way of creating another mini project to…]]></description><link>https://davidmason.hashnode.dev/creating-a-photo-geolocation-tool</link><guid isPermaLink="true">https://davidmason.hashnode.dev/creating-a-photo-geolocation-tool</guid><dc:creator><![CDATA[David]]></dc:creator><pubDate>Mon, 12 Oct 2026 10:41:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/65aca883-d142-4959-b93e-90673a48cb60.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A friend of mine had an idea for an outdoor tracking app based on a submitted Photo. I thought this was a great way of creating another mini project to help me cement the AWS services prior to my AWS Exam in November.</p>
<p>I would like to note that Ai does support in the development of my projects, but I feel this is important in 2026 where not using Ai would not be advantageous in terms of;</p>
<ol>
<li><p>Learning of new services and terminologies</p>
</li>
<li><p>Creation of quick to develop front end pages.</p>
</li>
</ol>
<p>Please note that whilst I use Ai, I am constantly learning the fundamentals in the background so I'm not just a mindless button pusher.</p>
<p>API Keys using KMS and IAM are not included in this project debrief but were setup initially.</p>
<p><strong>Step 1</strong></p>
<p>Using a very simple prompt, I created a photo uploader using .HTML, .CSS (and Tailwind) and Vanilla Javascript. The below code also includes our API Gateway URL which I will touch on next.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;The Wild Haven GreenScheme&lt;/title&gt;
  
  &lt;!-- 
    TAILWIND CSS CDN: 
    This script loads Tailwind CSS, a utility-first framework. 
    Instead of writing separate CSS files, we style elements directly in the HTML 
    using classes like 'text-center', 'bg-white', or 'rounded-xl'.
  --&gt;
  &lt;script src="https://cdn.tailwindcss.com"&gt;&lt;/script&gt;
  
  &lt;!-- 
    GOOGLE FONTS: 
    Loading a custom font called 'Plus Jakarta Sans' to make the app look modern.
  --&gt;
  &lt;link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet"&gt;
  
  &lt;!-- 
    CUSTOM STYLES: 
    A tiny bit of custom CSS to apply our chosen font to the whole body 
    and set a soft, nature-themed background color.
  --&gt;
  &lt;style&gt;
    body {
      font-family: 'Plus Jakarta Sans', sans-serif;
      background-color: #f0fdf4; /* Very light emerald green */
    }
  &lt;/style&gt;
&lt;/head&gt;

&lt;!-- 
  BODY ALIGNMENT: 
  'min-h-screen' ensures the body is at least as tall as the browser window.
  'flex items-center justify-center' pushes the main container perfectly into the middle of the screen.
--&gt;
&lt;body class="min-h-screen flex items-center justify-center p-4"&gt;

  &lt;!-- 
    MAIN CONTAINER: 
    This is the white card that holds everything. 
    'max-w-md w-full' ensures it doesn't get too wide on large screens but shrinks on mobile. 
    'rounded-2xl shadow-xl' gives it that modern app feel with soft corners and a drop shadow.
  --&gt;
  &lt;div class="max-w-md w-full bg-white rounded-2xl shadow-xl overflow-hidden border border-emerald-100 p-6 space-y-6"&gt;
    
    &lt;!-- HEADER SECTION --&gt;
    &lt;div class="text-center space-y-2"&gt;
      &lt;!-- Icon Container (A little leaf/nature icon) --&gt;
      &lt;div class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-emerald-100 text-emerald-600 mb-1"&gt;
        &lt;svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"&gt;
          &lt;path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" /&gt;
        &lt;/svg&gt;
      &lt;/div&gt;
      &lt;h1 class="text-2xl font-bold text-emerald-950"&gt;The Wild Haven GreenScheme&lt;/h1&gt;
      &lt;p class="text-xs text-emerald-600 font-medium"&gt;Nature &amp; Wildlife Photo Location Mapper&lt;/p&gt;
    &lt;/div&gt;

    &lt;!-- UPLOAD BOX AREA --&gt;
    &lt;div class="space-y-4"&gt;
      &lt;!-- 
        DROP ZONE: 
        This is the dashed box. We use JavaScript later so that clicking anywhere 
        inside this box triggers the hidden file input inside it.
      --&gt;
      &lt;div id="drop-zone" class="border-2 border-dashed border-emerald-300 rounded-xl p-6 text-center hover:border-emerald-500 transition-colors cursor-pointer bg-emerald-50/50 relative"&gt;
        
        &lt;!-- 
          HIDDEN FILE INPUT: 
          Default browser file inputs look ugly. We hide this one completely (class="hidden") 
          and use JavaScript to virtually 'click' it when the user clicks the drop zone.
        --&gt;
        &lt;input type="file" id="photo-input" accept="image/*" class="hidden"&gt;
        
        &lt;!-- 
          DEFAULT EMPTY STATE: 
          This is what the user sees before picking a photo (the icon and text).
        --&gt;
        &lt;div id="upload-prompt" class="space-y-2"&gt;
          &lt;div class="mx-auto w-10 h-10 text-emerald-500 flex items-center justify-center rounded-full bg-emerald-100"&gt;
            &lt;svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"&gt;
              &lt;path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /&gt;
            &lt;/svg&gt;
          &lt;/div&gt;
          &lt;p class="text-sm font-semibold text-emerald-900"&gt;Click to upload photo&lt;/p&gt;
          &lt;p class="text-xs text-emerald-600"&gt;JPG or PNG (Max 1 photo)&lt;/p&gt;
        &lt;/div&gt;

        &lt;!-- 
          IMAGE PREVIEW STATE: 
          This is completely hidden (class="hidden") when the page loads. 
          JavaScript will unhide this and hide the 'upload-prompt' once a photo is chosen.
        --&gt;
        &lt;div id="preview-container" class="hidden relative"&gt;
          &lt;!-- The actual image preview --&gt;
          &lt;img id="image-preview" src="" alt="Selected Photo" class="max-h-48 mx-auto rounded-lg object-cover shadow-sm"&gt;
          
          &lt;!-- The little 'X' button to remove the photo --&gt;
          &lt;button id="remove-btn" class="absolute top-1 right-1 bg-white/80 hover:bg-white text-gray-700 rounded-full p-1 shadow transition"&gt;
            &lt;svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"&gt;
              &lt;path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /&gt;
            &lt;/svg&gt;
          &lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;!-- 
        ERROR BANNER: 
        Hidden by default. JavaScript will inject error messages here if something goes wrong.
      --&gt;
      &lt;div id="error-message" class="hidden p-3 bg-red-50 text-red-700 rounded-lg text-xs font-medium border border-red-200"&gt;&lt;/div&gt;

      &lt;!-- 
        ACTION BUTTON: 
        Starts 'disabled' (unclickable) until a photo is chosen. 
        It contains a text span and a hidden spinner div for when it's loading.
      --&gt;
      &lt;button id="upload-btn" disabled class="w-full py-3 px-4 bg-emerald-600 hover:bg-emerald-700 disabled:bg-emerald-200 text-white font-semibold rounded-xl shadow transition-all duration-200 flex items-center justify-center space-x-2"&gt;
        &lt;span id="btn-text"&gt;Upload Photo&lt;/span&gt;
        &lt;div id="btn-spinner" class="hidden w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"&gt;&lt;/div&gt;
      &lt;/button&gt;
    &lt;/div&gt;

    &lt;!-- 
      LOCATION MODAL RESULT CARD: 
      Hidden by default. Appears automatically after the S3 upload finishes.
    --&gt;
    &lt;div id="location-modal" class="hidden p-4 bg-emerald-50 rounded-xl border border-emerald-200 space-y-2 animate-fade-in"&gt;
      &lt;div class="flex items-center space-x-2 text-emerald-800 font-semibold text-sm"&gt;
        &lt;svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-emerald-600 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"&gt;
          &lt;path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /&gt;
          &lt;path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /&gt;
        &lt;/svg&gt;
        &lt;span&gt;Photo Location Result&lt;/span&gt;
      &lt;/div&gt;
      
      &lt;!-- Where the actual city/country text gets injected --&gt;
      &lt;div id="location-text" class="text-base font-bold text-emerald-950 pl-7"&gt;
        Analyzing photo metadata...
      &lt;/div&gt;
      
      &lt;!-- Subtitle showing the current status of the database poll --&gt;
      &lt;p id="location-status" class="text-xs text-emerald-600 pl-7 italic"&gt;
        Polling DynamoDB background processor...
      &lt;/p&gt;
    &lt;/div&gt;

  &lt;/div&gt;

  &lt;!-- JAVASCRIPT SECTION: This makes the page interactive --&gt;
  &lt;script&gt;
    /*
      ===============================================================
      1. CONFIGURATION
      ===============================================================
      This is the URL to your AWS API Gateway. 
      It acts as the front door for your backend Lambda functions.
    */
    const API_BASE_URL = 'https://ohpzu9vdr9.execute-api.eu-west-2.amazonaws.com';

    /*
      ===============================================================
      2. DOM ELEMENT SELECTION
      ===============================================================
      Here we grab references to all the HTML elements we need to 
      manipulate (hide, show, change text, etc.) using their IDs.
    */
    const dropZone = document.getElementById('drop-zone');
    const photoInput = document.getElementById('photo-input');
    const uploadPrompt = document.getElementById('upload-prompt');
    const previewContainer = document.getElementById('preview-container');
    const imagePreview = document.getElementById('image-preview');
    const removeBtn = document.getElementById('remove-btn');
    const uploadBtn = document.getElementById('upload-btn');
    const btnText = document.getElementById('btn-text');
    const btnSpinner = document.getElementById('btn-spinner');
    const errorMessage = document.getElementById('error-message');
    const locationModal = document.getElementById('location-modal');
    const locationText = document.getElementById('location-text');
    const locationStatus = document.getElementById('location-status');

    // This variable will temporarily hold the image file the user selects.
    let selectedFile = null;

    /*
      ===============================================================
      3. USER INTERACTION EVENT LISTENERS
      ===============================================================
    */

    // A. Clicking the drop zone opens the computer's file browser
    dropZone.addEventListener('click', (e) =&gt; {
      // Prevent this from triggering if they actually clicked the 'X' remove button inside it
      if (e.target.closest('#remove-btn')) return; 
      
      // Virtually click the hidden HTML file input
      photoInput.click(); 
    });

    // B. Handling the file once the user selects it from their computer
    photoInput.addEventListener('change', (e) =&gt; {
      if (e.target.files.length &gt; 0) {
        selectedFile = e.target.files[0]; // Save the file to our variable
        
        // FileReader is a browser tool that reads the file so we can show a quick preview
        const reader = new FileReader();
        reader.onload = (event) =&gt; {
          // Put the raw image data into the &lt;img&gt; tag
          imagePreview.src = event.target.result; 
          
          // Hide the "Click to upload" text and show the image preview
          uploadPrompt.classList.add('hidden');
          previewContainer.classList.remove('hidden');
          
          // Turn on the big green Upload button
          uploadBtn.disabled = false;
          hideError();
          locationModal.classList.add('hidden');
        };
        // Command the reader to start reading the file
        reader.readAsDataURL(selectedFile);
      }
    });

    // C. Handling the 'X' remove button click
    removeBtn.addEventListener('click', (e) =&gt; {
      e.stopPropagation(); // Stops the dropZone click event from firing underneath it
      resetForm();         // Call our reset helper function
    });

    /*
      ===============================================================
      4. HELPER FUNCTIONS
      ===============================================================
    */

    // Puts the UI completely back to its starting state
    function resetForm() {
      selectedFile = null;
      photoInput.value = '';
      imagePreview.src = '';
      uploadPrompt.classList.remove('hidden');
      previewContainer.classList.add('hidden');
      uploadBtn.disabled = true;
      hideError();
      locationModal.classList.add('hidden');
    }

    function showError(msg) {
      errorMessage.textContent = msg;
      errorMessage.classList.remove('hidden');
    }

    function hideError() {
      errorMessage.classList.add('hidden');
    }

    /*
      ===============================================================
      5. CORE UPLOAD LOGIC
      ===============================================================
      This function runs when the user clicks "Upload Photo".
      Because it talks to servers, we use 'async' so the browser 
      doesn't freeze while waiting for responses.
    */
    uploadBtn.addEventListener('click', async () =&gt; {
      if (!selectedFile) return; // Safety check

      // Update UI to show Loading State
      uploadBtn.disabled = true;
      btnText.textContent = 'Uploading...';
      btnSpinner.classList.remove('hidden');
      hideError();
      locationModal.classList.add('hidden');

      try {
        /*
          STEP 1: Get the "Key" to upload.
          We ask our API Gateway / Lambda for a 'Presigned URL'.
          This is a temporary, secure link that allows us to bypass our server 
          and drop the image directly into the Amazon S3 bucket.
        */
        const presignedRes = await fetch(`${API_BASE_URL}/get-upload-url`);
        if (!presignedRes.ok) throw new Error('Failed to get presigned upload URL.');
        
        // Unpack the JSON response to get the URL and the generated filename
        const { uploadURL, fileName } = await presignedRes.json();

        /*
          STEP 2: Upload directly to S3.
          We use a 'PUT' request to send the heavy binary image file straight to AWS Storage.
        */
        const uploadRes = await fetch(uploadURL, {
          method: 'PUT',
          body: selectedFile
        });

        if (!uploadRes.ok) throw new Error('Failed to upload image directly to S3.');

        /*
          STEP 3: S3 Upload is done. Update UI.
          Now we wait for the background Lambda (process-photo-metadata) to do its job.
        */
        btnText.textContent = 'Upload Complete!';
        btnSpinner.classList.add('hidden');
        locationModal.classList.remove('hidden');
        locationText.textContent = 'Analyzing photo metadata...';
        locationStatus.textContent = 'Extracting EXIF GPS coordinates...';

        /*
          STEP 4: Start asking the database if it's done yet.
        */
        pollForLocation(fileName);

      } catch (err) {
        // If anything fails in the steps above, catch the error and show it
        console.error(err);
        showError(err.message || 'An error occurred during upload.');
        
        // Reset the button so they can try again
        uploadBtn.disabled = false;
        btnText.textContent = 'Upload Photo';
        btnSpinner.classList.add('hidden');
      }
    });

    /*
      ===============================================================
      6. DATABASE POLLING LOGIC
      ===============================================================
      Because the backend Lambda processes the EXIF data in the background,
      the frontend doesn't know exactly when it finishes.
      
      "Polling" means the frontend repeatedly asks the database (every 2 seconds) 
      "Are you done yet?" until it gets the answer or times out.
    */
    async function pollForLocation(fileName, attempts = 0) {
      // If we've asked 10 times (20 seconds) and still no answer, give up.
      if (attempts &gt;= 10) {
        locationText.textContent = 'Location unknown';
        locationStatus.textContent = 'Timed out waiting for location processing.';
        return;
      }

      try {
        // Ask API Gateway to check DynamoDB for this specific file name
        const res = await fetch(`${API_BASE_URL}/get-location?fileName=${encodeURIComponent(fileName)}`);
        
        if (res.ok) {
          const data = await res.json();
          
          // If the backend has replaced the temporary "Processing..." text with a real location
          if (data.location &amp;&amp; data.location !== 'Processing EXIF metadata...') {
            
            // Success! Update the UI with the final location.
            locationText.textContent = data.location;
            locationStatus.textContent = 'Retrieved directly from DynamoDB';
            return; // Stop the polling loop completely
          }
        }
      } catch (err) {
        console.warn('Polling check error:', err);
      }

      // If we didn't return (stop) above, wait 2000 milliseconds (2 seconds) 
      // and then call this exact same function again, adding 1 to our attempts counter.
      setTimeout(() =&gt; pollForLocation(fileName, attempts + 1), 2000);
    }
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p><strong>Step 2.</strong></p>
<p>I used API Gateway as the entry point into my AWS backed but as API Gateway is not suitable for high res photos going through it (more costly and prone to timeouts).</p>
<p>So instead of passing the image through the API, a lambda programme (<code>get-presigned-url</code>) acted as the gatekeeper to allow a photo in without exposing my API Keys.</p>
<pre><code class="language-python">import json
import boto3
import uuid
import os
from botocore.config import Config

s3_client = boto3.client('s3', config=Config(signature_version='s3v4'))
BUCKET_NAME = os.environ['BUCKET_NAME']

def lambda_handler(event, context):
    try:
        file_name = f"{uuid.uuid4()}.jpg"
        
        # Generate URL without strict content-type signature lock
        presigned_url = s3_client.generate_presigned_url(
            'put_object',
            Params={
                'Bucket': BUCKET_NAME,
                'Key': file_name
            },
            ExpiresIn=300
        )
        
        return {
            'statusCode': 200,
            'headers': {
                'Access-Control-Allow-Origin': '*',
                'Access-Control-Allow-Headers': '*',
                'Access-Control-Allow-Methods': 'GET, PUT, OPTIONS'
            },
            'body': json.dumps({
                'uploadURL': presigned_url,
                'fileName': file_name
            })
        }
    except Exception as e:
        print(f"Error: {e}")
        return {
            'statusCode': 500,
            'body': json.dumps({'error': 'Could not generate URL'})
        }
</code></pre>
<p>This script was also adapted to Access-Control-Allow-Origin so browsers wouldn't block it.</p>
<p><strong>Step 3.</strong></p>
<p>The photo is then uploaded to an <strong>S3 Bucket</strong> which has an Even notification attached to it, which will trigger another Lambda function</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/c6258638-63ed-424a-b300-0a6eeb4bdae8.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 4.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/05be39bd-1b63-439e-b827-f793f6010591.png" alt="" style="display:block;margin:0 auto" />

<p>The lambda function <strong>process-photo-metadata</strong> has now been triggered and this will begin to assess the photo's metadata and where the photo was taken. This uses EXIF Binary Parsing which I am currently not familiar with to show the exact GPS. This then writes the location to the DynamoDB table</p>
<p><strong>Step 5.</strong></p>
<p>In our HTML file, we have a Javascript function that begins polling the AWS Api for the result of the Photo. It will run 10 times before timing out to prevent overuse.</p>
<pre><code class="language-plaintext">  6. DATABASE POLLING LOGIC
      ===============================================================
      Because the backend Lambda processes the EXIF data in the background,
      the frontend doesn't know exactly when it finishes.
      
      "Polling" means the frontend repeatedly asks the database (every 2 seconds) 
      "Are you done yet?" until it gets the answer or times out.
    */
    async function pollForLocation(fileName, attempts = 0) {
      // If we've asked 10 times (20 seconds) and still no answer, give up.
      if (attempts &gt;= 10) {
        locationText.textContent = 'Location unknown';
        locationStatus.textContent = 'Timed out waiting for location processing.';
        return;
      }

      try {
        // Ask API Gateway to check DynamoDB for this specific file name
        const res = await fetch(`${API_BASE_URL}/get-location?fileName=${encodeURIComponent(fileName)}`);
        
        if (res.ok) {
          const data = await res.json();
          
          // If the backend has replaced the temporary "Processing..." text with a real location
          if (data.location &amp;&amp; data.location !== 'Processing EXIF metadata...') {
            
            // Success! Update the UI with the final location.
            locationText.textContent = data.location;
            locationStatus.textContent = 'Retrieved directly from DynamoDB';
            return; // Stop the polling loop completely
          }
        }
      } catch (err) {
        console.warn('Polling check error:', err);
      }

      // If we didn't return (stop) above, wait 2000 milliseconds (2 seconds) 
      // and then call this exact same function again, adding 1 to our attempts counter.
      setTimeout(() =&gt; pollForLocation(fileName, attempts + 1), 2000);
    }
</code></pre>
<p>This is the Lambda code that processes such a polling request</p>
<pre><code class="language-python">import json
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('photo-metadata')

def lambda_handler(event, context):
    # Get fileName from query parameters: /get-location?fileName=xyz.jpg
    params = event.get('queryStringParameters') or {}
    file_name = params.get('fileName')
    
    if not file_name:
        return {
            'statusCode': 400,
            'headers': {'Access-Control-Allow-Origin': '*'},
            'body': json.dumps({'error': 'Missing fileName parameter'})
        }
        
    try:
        response = table.get_item(Key={'fileName': file_name})
        item = response.get('Item')
        
        if item:
            return {
                'statusCode': 200,
                'headers': {
                    'Access-Control-Allow-Origin': '*',
                    'Access-Control-Allow-Methods': 'GET, OPTIONS'
                },
                'body': json.dumps({'location': item.get('location', 'Location processing...')})
            }
        else:
            return {
                'statusCode': 200,
                'headers': {'Access-Control-Allow-Origin': '*'},
                'body': json.dumps({'location': 'Processing EXIF metadata...'})
            }
    except Exception as e:
        print(f"Error: {e}")
        return {
            'statusCode': 500,
            'headers': {'Access-Control-Allow-Origin': '*'},
            'body': json.dumps({'error': 'Failed to fetch location'})
        }
</code></pre>
<p>This then results in a resulting image having a location.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/dd363e8d-afab-47d1-8b7f-d8ae8b521eff.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Future Considerations</strong></p>
<p>This has been a fairly straightforward example to achieve. I am not saying I understand how the code between AWS and my Frontend is linking yet but this is just the beginning.</p>
<p>From here I could develop this further by creating a login area where someone can upload a photo, it would save to their account.</p>
]]></content:encoded></item><item><title><![CDATA[Combining Sandbox Payment Processor with AWS]]></title><description><![CDATA[As I now had a working prototype based in VS Code Python Script, I was eager to apply my AWS modest learnings with this problem. Step 1. I added my API Key…]]></description><link>https://davidmason.hashnode.dev/combining-sandbox-payment-processor-with-aws</link><guid isPermaLink="true">https://davidmason.hashnode.dev/combining-sandbox-payment-processor-with-aws</guid><dc:creator><![CDATA[David]]></dc:creator><pubDate>Thu, 08 Oct 2026 13:47:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/aa0a6026-a538-462d-89c6-75f30de1faf4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As I now had a working prototype based in VS Code Python Script, I was eager to apply my AWS modest learnings with this problem.</p>
<p><strong>Step 1.</strong></p>
<p>I added my API Key and ID into AWS Secrets Manager under other type of secret.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/e28b64db-b134-42fa-8638-1b7164b3eab8.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Step 2.</strong></p>
<p>With the support of AI for speed, I created a very modest index.html which consisted of a form with 2 questions</p>
<ol>
<li><p>Item Name</p>
</li>
<li><p>Item Price</p>
</li>
</ol>
<p>(The Final Code is at the bottom)</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/70301055-70df-4e32-9d12-aa678ecd32a0.png" alt="" style="display:block;margin:0 auto" />

<p>The script used a Javascript Script to fire the submission</p>
<pre><code class="language-plaintext">&lt;script&gt;
        document.getElementById("paymentForm").addEventListener("submit", async function(event) {
            event.preventDefault(); 

            // Make sure this is your actual API Gateway URL ending in /pay
            const apiUrl = "https://vyle17q52f.execute-api.eu-west-2.amazonaws.com/pay"; 
            
            const formData = new URLSearchParams(new FormData(this));

            try {
                const response = await fetch(apiUrl, {
                    method: "POST",
                    body: formData
                });

                const result = await response.json();
                
                // If an action URL exists, redirect the browser to complete 3DS
                if (result.action_url) {
                    window.location.href = result.action_url;
                } else if (result.cashflows_status === 200 || result.cashflows_status === 201) {
                    alert("Success! Payment approved.");
                } else if (result.response &amp;&amp; result.response.errorReport) {
                    alert("Payment Failed:\n" + result.response.errorReport.errors[0].message);
                } else {
                    alert("Payment Error:\n" + JSON.stringify(result));
                }
                
            } catch (error) {
                alert("Network error: Could not reach the server.");
            }
        });
    &lt;/script&gt;
</code></pre>
<p><strong>Step 3.</strong></p>
<p>Using AWS Amplify, I linked by Git account and created a live site of my code. Using VS Code I will update that as we go through.</p>
<p><strong>Step 4.</strong></p>
<p>Using AWS IAM, I added a policy called '<strong>CashflowsSecretReadAccess</strong>' which gave the script access to the Secrets Manager and created a role for;</p>
<p><strong>AWSLambdaBasicExecutionRole</strong> - For Logging purposes</p>
<p><strong>Step 5.</strong></p>
<p>Using <strong>LAMBDA</strong> I added a Python script that ran when the form was submitted</p>
<pre><code class="language-plaintext">import json
import hashlib
import urllib.request
import urllib.parse
import base64
import boto3
from botocore.exceptions import ClientError

def get_secrets():
    client = boto3.client('secretsmanager', region_name="eu-west-2")
    try:
        response = client.get_secret_value(SecretId="cashflows/credentials")
        return json.loads(response['SecretString'])
    except ClientError as e:
        print(f"Error fetching secret: {e}")
        return None

def lambda_handler(event, context):
    # 1. Fetch credentials from AWS Secrets Manager
    secrets = get_secrets()
    if not secrets:
        return {
            "statusCode": 500, 
            "headers": {"Access-Control-Allow-Origin": "*", "Content-Type": "application/json"},
            "body": json.dumps({"message": "Failed to load credentials"})
        }
    
    api_key = secrets.get('API_KEY')
    config_id = secrets.get('CONFIG_ID')
    endpoint = "https://gateway-int.cashflows.com/api/gateway/payment-jobs"

    # 2. Extract data from incoming HTML form submission
    try:
        raw_body = event.get('body', '')
        if event.get('isBase64Encoded', False):
            raw_body = base64.b64decode(raw_body).decode('utf-8')
        
        parsed_body = urllib.parse.parse_qs(raw_body)
        amount = parsed_body.get('amountToCollect', ['10.00'])[0]
        order_number = parsed_body.get('orderNumber', ['Test-Order-01'])[0]
    except Exception as e:
        print(f"Extraction error: {e}")
        amount = "10.00"
        order_number = "Test-Order-01"

    # 3. Build Cashflows Payload using test card &amp; 3DS bypass name
    payload_dict = {
        "type": "Payment",
        "paymentMethodsToUse": ["creditcard"],
        "amountToCollect": str(amount),
        "currency": "GBP",
        "locale": "en_GB",
        "parameters": {
            "cardNumber": "4510400400099005",
            "cardHolderName": "Luke Skywalker",  # Triggers automatic 3DS success in test environment
            "cardCvc": "123",
            "cardExpiryMonth": "12",
            "cardExpiryYear": "28"
        },
        "order": {
            "orderNumber": str(order_number)
        }
    }

    # 4. Generate SHA-512 Hash
    raw_json_body = json.dumps(payload_dict, separators=(',', ':'))
    hash_input = api_key + raw_json_body
    request_hash = hashlib.sha512(hash_input.encode('utf-8')).hexdigest().upper()

    headers = {
        "ConfigurationId": config_id,
        "Hash": request_hash,
        "Content-Type": "application/json"
    }

    # 5. Send request to Cashflows
    req = urllib.request.Request(endpoint, data=raw_json_body.encode('utf-8'), headers=headers, method='POST')
    
    try:
        with urllib.request.urlopen(req) as response:
            response_data = json.loads(response.read().decode('utf-8'))
            status_code = response.getcode()
    except urllib.error.HTTPError as e:
        response_data = json.loads(e.read().decode('utf-8'))
        status_code = e.code

    # 6. Extract action URL if Cashflows returns a 3DS redirect link
    action_url = None
    if isinstance(response_data, dict):
        action_url = response_data.get("links", {}).get("action", {}).get("url")

    return {
        "statusCode": 200,
        "headers": {
            "Access-Control-Allow-Origin": "https://main.dna7h844op3tm.amplifyapp.com",
            "Content-Type": "application/json"
        },
        "body": json.dumps({
            "cashflows_status": status_code,
            "action_url": action_url,
            "response": response_data
        })
    }
</code></pre>
<p>Some important steps in this code:</p>
<ol>
<li><p>Luke Skywalker is a success message for testing purposes.</p>
</li>
<li><p>The Lambda will run for 15 seconds but it will fall back on fake data if it doesn't process it in time.</p>
</li>
</ol>
<p><strong>Step 6.</strong></p>
<p>Using AWS API Gateway, I create a POST gateway and linked it to my LAMBDA code with a deployed stage URL ending with /pay.</p>
<p>The gateway has a burst limit of 10 and a rate limit of 5 per second to help prevent bad actors.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/d333f238-26b2-412d-902e-ac9cc8aa14c3.png" alt="" style="display:block;margin:0 auto" />

<p>Cross-Origin Resource Sharing (<strong>CORS</strong>) is also directed to the Amplify URL to prevent the form being used from a different source.</p>
<p><strong>Step 7.</strong></p>
<p>Once uploaded, this now produces a working model.</p>
<h2><strong>Next Steps</strong></h2>
<p>This is just the start of this, as it is a very modest E-Commerce engine. We could;</p>
<ol>
<li><p>Add Products</p>
</li>
<li><p>Add Card Holder Details</p>
</li>
<li><p>Add CAPTCHA</p>
</li>
<li><p>Success and Failure redirects.</p>
</li>
</ol>
<h2>Challenges</h2>
<p>Working with AI, I managed to get the code working in some form very early on, but getting working Statuses was a challenge. Looking over the Specs, I found out I needed Luke Skywalker involved and to change my action_url link to one given by the Payment Software.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/16993291-48db-46a4-9ca7-dbe11586ae1a.png" alt="" style="display:block;margin:0 auto" />]]></content:encoded></item><item><title><![CDATA[API Calls Introduction]]></title><description><![CDATA[API Calls run the Internet. When you press Play watching your 5th Episode in a row of 'Great British Bake Off' on your Smart TV, the play button doesn't…]]></description><link>https://davidmason.hashnode.dev/api-calls-introduction</link><guid isPermaLink="true">https://davidmason.hashnode.dev/api-calls-introduction</guid><dc:creator><![CDATA[David]]></dc:creator><pubDate>Thu, 08 Oct 2026 09:05:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/f0c8a349-d466-4b1b-90b4-41a022ce6274.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>API Calls run the Internet. When you press Play watching your 5th Episode in a row of 'Great British Bake Off' on your Smart TV, the play button doesn't actually play the content.</p>
<p>Instead a packet of information is sent to the Media Servers to say this episode has been requested by user 123456. It will do authentication to check you have the right to watch that, then a success/sorry message will be relayed back resulting in HD Nigella.</p>
<p>I was kindly given access to a sandbox dev environment of an online Payment Solution.</p>
<p><strong>Step 1.</strong></p>
<p>In order to run an API, you need an API Key or a Personal Access Token. The first thing to note is, an API is a key and like your house keys, you wouldn't leave them out for anyone to take (I hope?).</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ac4f7f48571823aa9acce4d/891a6874-0d19-4d56-990a-16c7fe2c5580.png" alt="" style="display:block;margin:0 auto" />

<p>So API Keys need to be held safe either Internally of the Software or an Exterior Source such as <a href="https://www.linkedin.com/company/amazon-web-services/"><strong>Amazon Web Services (AWS)</strong></a> Secrets Manager or <a href="https://www.linkedin.com/company/keeper-security-inc-/"><strong>Keeper Security, Inc.</strong></a> They can't be naked in the code otherwise you're going to have a bad day.</p>
<p>API Keys also have different configurations in terms of what they can and can't do. Can they update something or can they just view something? Different softwares will give different abilities in terms of what is in the 'scope' of the API.</p>
<p><strong>Step 2.</strong></p>
<p>Using <a href="https://www.linkedin.com/company/learn-python-programming/"><strong>Python</strong></a>, we want to create a script which takes the API and sends over data in a specially configured JSON package.</p>
<p>Why Python? Python is great language for sending data in a clean format. It's a popular choice when it comes to Business Intelligence and Analytics and making sure the data is structured corrected. We don't want Dirty Data.</p>
<p>This is especially important with this platform as gaps in the payload will cause errors.</p>
<pre><code class="language-plaintext">import hashlib
import json
import requests

api_key = "Darth Vader is Luke's Dad"
config_id = "Shhh"
endpoint = ""

# 1. Define the payload
payload_dict = {
    "type": "Payment",
    "paymentMethodsToUse": ["creditcard"],
    "amountToCollect": "99.99",
    "SCAExemptionIndicator": "LowValue",
    "currency": "GBP",
    "locale": "en_GB",
    "parameters": {
        "cardNumber": "00",
        "cardHolderName": "Test User",
        "cardCvc": "123",
        "cardExpiryMonth": "12",
        "cardExpiryYear": "30"
    },
    "order": {
        "orderNumber": "Tea Pot"
    }
}

# 2. Convert dictionary to a compact JSON string (no extra spaces)
raw_json_body = json.dumps(payload_dict, separators=(',', ':'))

# 3. Generate the SHA-512 Hash: API Key + Raw JSON Body
hash_input = api_key + raw_json_body
request_hash = hashlib.sha512(hash_input.encode('utf-8')).hexdigest().upper()

# 4. Set Headers and fire the request
headers = {
    "ConfigurationId": config_id,
    "Hash": request_hash,
    "Content-Type": "application/json"
}

response = requests.post(endpoint, headers=headers, data=raw_json_body)

print(f"Status Code: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
</code></pre>
<p>This code has a cool feature called the sha512 in which the Order details along with the API are concatenated together to form a massive number. This is run through a hash generator to create a secret number that only the other side knows is true.</p>
<p>In order to run this code through the terminal I had to do;</p>
<pre><code class="language-plaintext">source .venv/bin/activate // Creates a local virtual environment for the Libraries as I haven't installed them fully yet.

python3 Desktop/test_payment.py - Run the code
</code></pre>
<p>I then get my order come through to the portal but it's pending?</p>
<p>Well this is because fintech is uber complex and we need to click on a confirmation page to confirm this. As of the time of writing this, I have managed to get this to work once but I don't fully get this yet.</p>
<p>But from an API call, I have triggered a payment.</p>
<p><strong>What did I learn from this?</strong></p>
<p>I don't fully understand Python code (yet) as I have been predominantly working in Javascript but I have understood the basics of the payload being sent.</p>
<p><strong>What's next?</strong></p>
<p>This currently runs from my desktop but I plan now to mock up a quick site where I add the key details such as the product, the price, the card details and it will do the API call directly.</p>
<p>Thank you for taking the time to read this.</p>
]]></content:encoded></item></channel></rss>